Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3af2210c5e | |||
| d6aa73fd28 | |||
| f0083cfe45 | |||
| 66f32dd678 | |||
| 7faf1e1366 | |||
| e6ae16ca51 | |||
| 6af5250501 | |||
| 7c0dd9a6cc | |||
| 5f6b2223e1 | |||
| 864157ac84 | |||
| c89ee5ddb3 | |||
| 5a255350b4 | |||
| 8919524432 | |||
| f79a64de60 | |||
| 3fdeccdb23 |
@@ -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}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
name: Build and Deploy
|
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
|
||||||
|
|
||||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
deployments: write
|
|
||||||
|
|
||||||
# Allow one concurrent deployment
|
|
||||||
concurrency:
|
|
||||||
group: "pages"
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
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"
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Setup Pages
|
|
||||||
uses: actions/configure-pages@v2
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v1
|
|
||||||
with:
|
|
||||||
# Upload entire repository
|
|
||||||
path: "./website/dist"
|
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v1
|
|
||||||
@@ -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
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18]
|
node-version: [16, 17]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
version: 7
|
node-version: ${{ matrix.node-version }}
|
||||||
- uses: actions/setup-node@v3
|
cache: 'npm'
|
||||||
with:
|
- run: npm install
|
||||||
node-version: ${{ matrix.node-version }}
|
- run: npm test
|
||||||
cache: 'pnpm'
|
|
||||||
- run: pnpm install
|
|
||||||
- run: pnpm run format-check
|
|
||||||
- run: pnpm run lint
|
|
||||||
- run: pnpm test
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
dist
|
dist
|
||||||
out
|
|
||||||
!docs/dist
|
!docs/dist
|
||||||
dist-*
|
dist-*
|
||||||
cabal-dev
|
cabal-dev
|
||||||
@@ -27,20 +26,4 @@ node_modules/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
repl-parcel
|
repl-parcel
|
||||||
mytunes.ts
|
mytunes.ts
|
||||||
doc
|
doc
|
||||||
out
|
|
||||||
.parcel-cache
|
|
||||||
repl_old
|
|
||||||
tutorial.rendered.mdx
|
|
||||||
doc.json
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"bracketSameLine": false,
|
"jsxBracketSameLine": false,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"endOfLine": "lf"
|
"endOfLine": "lf"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,9 +2,5 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"subspan",
|
"subspan",
|
||||||
"vals"
|
"vals"
|
||||||
],
|
]
|
||||||
"yaml.schemas": {
|
|
||||||
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
|
||||||
},
|
|
||||||
"testing.automaticallyOpenPeekView": "never"
|
|
||||||
}
|
}
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# 🌀 Contributing to Strudel 🌀
|
|
||||||
|
|
||||||
Thanks for wanting to contribute!!! There are many ways you can add value to this project
|
|
||||||
|
|
||||||
## Communication Channels
|
|
||||||
|
|
||||||
To get in touch with the contributors, either
|
|
||||||
|
|
||||||
- open a [github discussion](https://github.com/tidalcycles/strudel/discussions) or
|
|
||||||
- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel
|
|
||||||
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
|
|
||||||
|
|
||||||
## Ask a Question
|
|
||||||
|
|
||||||
If you have any questions about strudel, make sure you've glanced through the
|
|
||||||
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
|
||||||
If not, use one of the Communication Channels above!
|
|
||||||
|
|
||||||
Don't be afraid to ask! Your question might be of great value for other people too.
|
|
||||||
|
|
||||||
## Give Feedback
|
|
||||||
|
|
||||||
No matter if you've used the Strudel REPL or if you are using the strudel packages, we are happy to hear some feedback.
|
|
||||||
Use one of the Communication Channels listed above and drop us a line or two!
|
|
||||||
|
|
||||||
## Share Music
|
|
||||||
|
|
||||||
If you made some music with strudel, you can give back some love and share what you've done!
|
|
||||||
Your creation could also be part of the random selection in the REPL if you want.
|
|
||||||
Use one of the Communication Channels listed above.
|
|
||||||
|
|
||||||
## Improve the Docs
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Propose a Feature
|
|
||||||
|
|
||||||
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above.
|
|
||||||
Maybe you even want to help with the implementation of that feature!
|
|
||||||
|
|
||||||
## Report a Bug
|
|
||||||
|
|
||||||
If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://github.com/tidalcycles/strudel/issues).
|
|
||||||
Please check that it has not been reported before.
|
|
||||||
|
|
||||||
## Fix a Bug
|
|
||||||
|
|
||||||
To fix a bug that has been reported,
|
|
||||||
|
|
||||||
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
|
|
||||||
2. fork the repository
|
|
||||||
3. make sure you've setup the project (see below)
|
|
||||||
4. hopefully fix the bug
|
|
||||||
5. make sure the tests pass
|
|
||||||
6. send a pull request
|
|
||||||
|
|
||||||
## Write Tests
|
|
||||||
|
|
||||||
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
|
||||||
You can find the tests in each package in the `test` folder. To run all tests, run `pnpm 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`)
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
Those commands might look slightly different for your OS.
|
|
||||||
Please report any problems you've had with the setup instructions!
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
|
||||||
|
|
||||||
- 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)
|
|
||||||
2. open command palette and run "Format Document With..."
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Package Publishing
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Have Fun
|
|
||||||
|
|
||||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,15 +7,17 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works, specifically designed to ensure
|
software and other kinds of works.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users.
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
To protect your rights, we need to prevent others from denying you
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
you this License which gives you legal permission to copy, distribute
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
and/or modify the software.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
For example, if you distribute copies of such a program, whether
|
||||||
improvements made in alternate versions of the program, if they
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
receive widespread use, become available for other developers to
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
incorporate. Many developers of free software are heartened and
|
or can get the source code. And you must show them these terms so they
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
know their rights.
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
ensure that, in such cases, the modified source code becomes available
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
to the community. It requires the operator of a network server to
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that there is no warranty for this free software. For both users' and
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
changed, so that their problems will not be attributed erroneously to
|
||||||
this license.
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -60,7 +72,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU General Public License into a single
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the work with which it is combined will remain governed by version
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
If the program does terminal interaction, make it output a short
|
||||||
network, you should also make sure that it provides a way for users to
|
notice like this when it starts in an interactive mode:
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
<program> Copyright (C) <year> <name of author>
|
||||||
of the code. There are many ways you could offer source, and different
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
solutions will be better for different programs; see section 13 for the
|
This is free software, and you are welcome to redistribute it
|
||||||
specific requirements.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|||||||
@@ -2,53 +2,42 @@
|
|||||||
|
|
||||||
[](https://github.com/tidalcycles/strudel/actions)
|
[](https://github.com/tidalcycles/strudel/actions)
|
||||||
|
|
||||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully.
|
||||||
|
|
||||||
- Try it here: <https://strudel.tidalcycles.org/>
|
Try it here: https://strudel.tidalcycles.org/
|
||||||
- Docs: <https://strudel.tidalcycles.org/learn/>
|
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
|
||||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
|
||||||
|
|
||||||
## Running Locally
|
Tutorial: https://strudel.tidalcycles.org/tutorial/
|
||||||
|
|
||||||
After cloning the project, you can run the REPL locally:
|
## Local development
|
||||||
|
|
||||||
|
Run the REPL locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run setup
|
npm install
|
||||||
pnpm run repl
|
npx lerna bootstrap
|
||||||
|
cd repl
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Publish Packages
|
||||||
|
|
||||||
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
|
To publish, just run:
|
||||||
|
|
||||||
- [`core`](./packages/core/): tidal pattern engine
|
```sh
|
||||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
npx lerna version
|
||||||
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
|
```
|
||||||
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
|
|
||||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
|
||||||
- [`midi`](./packages/midi): webmidi bindings
|
|
||||||
- [`serial`](./packages/serial): webserial bindings
|
|
||||||
- [`tonal`](./packages/tonal): tonal functions
|
|
||||||
- [`xen`](./packages/xen): microtonal / xenharmonic functions
|
|
||||||
- ... [and there are more](./packages/)
|
|
||||||
|
|
||||||
Click on the package names to find out more about each one.
|
This will publish all packages that changed since the last version.
|
||||||
|
|
||||||
## Contributing
|
## Style
|
||||||
|
|
||||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
For now, please try to copy the style of surrounding code. VS Code users can install the 'prettier' add-on which will use the .prettierrc configuration file for automatic formatting.
|
||||||
|
|
||||||
<a href="https://github.com/tidalcycles/strudel/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
Made with [contrib.rocks](https://contrib.rocks).
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
|
There is a #strudel channel on the TidalCycles discord: https://discord.com/invite/HGEdXmRkzT
|
||||||
|
|
||||||
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
|
You can also ask questions and find related discussions on the tidal club forum: https://club.tidalcycles.org/
|
||||||
|
|
||||||
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
||||||
|
Before Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"main.css": "/static/css/main.0d689283.css",
|
||||||
|
"main.js": "/static/js/main.77e38ada.js",
|
||||||
|
"static/js/787.8f7ec9e0.chunk.js": "/static/js/787.8f7ec9e0.chunk.js",
|
||||||
|
"static/media/logo.svg": "/static/media/logo.ac95051720b3dccfe511e0e02d8e1029.svg",
|
||||||
|
"index.html": "/index.html",
|
||||||
|
"main.0d689283.css.map": "/static/css/main.0d689283.css.map",
|
||||||
|
"main.77e38ada.js.map": "/static/js/main.77e38ada.js.map",
|
||||||
|
"787.8f7ec9e0.chunk.js.map": "/static/js/787.8f7ec9e0.chunk.js.map"
|
||||||
|
},
|
||||||
|
"entrypoints": [
|
||||||
|
"static/css/main.0d689283.css",
|
||||||
|
"static/js/main.77e38ada.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.77e38ada.js"></script><link href="/static/css/main.0d689283.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";(self.webpackChunk_strudel_repl=self.webpackChunk_strudel_repl||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
|
||||||
|
//# sourceMappingURL=787.8f7ec9e0.chunk.js.map
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
object-assign
|
||||||
|
(c) Sindre Sorhus
|
||||||
|
@license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license Fraction.js v4.2.0 05/03/2022
|
||||||
|
* https://www.xarg.org/2014/03/rational-numbers-in-javascript/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021, Robert Eisele (robert@xarg.org)
|
||||||
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tone.js
|
||||||
|
* @author Yotam Mann
|
||||||
|
* @license http://opensource.org/licenses/MIT MIT License
|
||||||
|
* @copyright 2014-2019 Yotam Mann
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v0.20.2
|
||||||
|
* scheduler.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v17.0.2
|
||||||
|
* react-dom.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v17.0.2
|
||||||
|
* react-jsx-runtime.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @license React v17.0.2
|
||||||
|
* react.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.999678aa.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.097d520a.js" defer></script> </body></html>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Tidal Features in Strudel
|
||||||
|
|
||||||
|
## Basics
|
||||||
|
|
||||||
|
- [ ] drawLine
|
||||||
|
- [ ] setcps
|
||||||
|
- [ ] naming patterns? block based evaluation?
|
||||||
|
- [ ] once
|
||||||
|
- [ ] silence
|
||||||
|
- [x] hush
|
||||||
|
- [ ] panic
|
||||||
|
|
||||||
|
## Concatenation
|
||||||
|
|
||||||
|
https://tidalcycles.org/docs/patternlib/tour/concatenation
|
||||||
|
|
||||||
|
- [x] cat: is synonym to fastcat in strudel, while [in tidal, cat is slowcat](https://tidalcycles.org/docs/patternlib/tour/concatenation#cat)
|
||||||
|
- [x] fastcat
|
||||||
|
- [x] timeCat: why is this camel case?
|
||||||
|
- [ ] randcat
|
||||||
|
- [x] append: but is like fastAppend in tidal
|
||||||
|
- [ ] fastAppend
|
||||||
|
- [ ] slowAppend
|
||||||
|
- [ ] wedge
|
||||||
|
- [ ] brak
|
||||||
|
- [ ] flatpat
|
||||||
|
|
||||||
|
## Accumulation
|
||||||
|
|
||||||
|
- [ ] overlay => like stack? "The overlay function is similar to cat" => wrong?
|
||||||
|
- [ ] `<>` operator (=== overlay)
|
||||||
|
- [x] stack
|
||||||
|
- [x] superimpose => strudel supports multiple args => is this layer?
|
||||||
|
- [ ] layer => is this like superimpose but with multiple args?
|
||||||
|
- [ ] steps ?
|
||||||
|
- [ ] iter, iter'
|
||||||
|
|
||||||
|
## Alteration
|
||||||
|
|
||||||
|
- [ ] range, rangex
|
||||||
|
- [ ] quantise
|
||||||
|
- [ ] ply
|
||||||
|
- [ ] stutter
|
||||||
|
- [ ] stripe, slowstripe
|
||||||
|
- [ ] palindrome = every(2, rev)
|
||||||
|
- [ ] trunc
|
||||||
|
- [ ] linger
|
||||||
|
- [ ] chunk, chunk'
|
||||||
|
- [ ] shuffle
|
||||||
|
- [ ] scramble
|
||||||
|
- [ ] rot
|
||||||
|
- [ ] step / step'
|
||||||
|
- [ ] lindenmeyer
|
||||||
|
- [ ] spread / spreadf / fastspread
|
||||||
|
- [ ] spreadChoose / spreadr
|
||||||
|
|
||||||
|
## conditions
|
||||||
|
|
||||||
|
- [x] every
|
||||||
|
- [ ] every'
|
||||||
|
- [ ] whenmod
|
||||||
|
- [ ] sometimes, sometimesBy, someCycles, someCyclesBy
|
||||||
|
- [ ] choose, chooseby, wchoose, wchooseby
|
||||||
|
- [x] struct
|
||||||
|
- [x] mask
|
||||||
|
- [ ] sew
|
||||||
|
- [ ] stitch
|
||||||
|
- [ ] select, selectF
|
||||||
|
- [ ] pickF
|
||||||
|
- [ ] squeeze
|
||||||
|
- [ ] euclid, euclidInv, euclidFull
|
||||||
|
- [ ] ifp
|
||||||
|
|
||||||
|
## Time
|
||||||
|
|
||||||
|
- [x] fast
|
||||||
|
- [x] fastGap
|
||||||
|
- [x] slow
|
||||||
|
- [ ] hurry
|
||||||
|
- [ ] compress: is this compressSpan?
|
||||||
|
- [ ] zoom
|
||||||
|
- [ ] within
|
||||||
|
- [x] off
|
||||||
|
- [ ] rotL / rotR
|
||||||
|
- [x] rev
|
||||||
|
- [ ] jux / juxBy
|
||||||
|
- [ ] swingBy / swing
|
||||||
|
- [ ] ghost
|
||||||
|
- [ ] inside / outside
|
||||||
|
|
||||||
|
## Harmony & Melody
|
||||||
|
|
||||||
|
- [x] scale
|
||||||
|
- [ ] scaleList
|
||||||
|
- [ ] getScale
|
||||||
|
- [ ] toScale
|
||||||
|
- [ ] chordList
|
||||||
|
- [ ] arpeggiate
|
||||||
|
- [ ] arp
|
||||||
|
|
||||||
|
## Transitions
|
||||||
|
|
||||||
|
- [ ] anticipate / anticipateIn
|
||||||
|
- [ ] clutch / clutchIn
|
||||||
|
- [ ] histpan
|
||||||
|
- [ ] interpolate / interpolateIn
|
||||||
|
- [ ] jump / jumpIn / jumpIn' / jumpMod
|
||||||
|
- [ ] wait / waitT
|
||||||
|
- [ ] wash / washIn
|
||||||
|
- [ ] xfade / xfadeIn
|
||||||
|
|
||||||
|
## Sampling
|
||||||
|
|
||||||
|
- [ ] chop
|
||||||
|
- [ ] striate / striateBy
|
||||||
|
- [ ] loopAt
|
||||||
|
- [ ] segment
|
||||||
|
- [ ] discretise
|
||||||
|
|
||||||
|
## Randomness
|
||||||
|
|
||||||
|
- [ ] rand / irand
|
||||||
|
- [ ] perlin / perlinWith / perlin2 / perlin2With
|
||||||
|
|
||||||
|
## Composition
|
||||||
|
|
||||||
|
- [ ] ur
|
||||||
|
- [ ] seqP / seqPLoop
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script>import("./strudel.js").then(m => strudel = m)
|
||||||
|
</script>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Bingo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button onclick="foo()">hello</button>
|
||||||
|
<button onclick="bar()">hello</button>
|
||||||
|
|
||||||
|
<script type="module" src="./strudel.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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,11 +0,0 @@
|
|||||||
{
|
|
||||||
"source": {
|
|
||||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
|
||||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
|
||||||
},
|
|
||||||
"plugins": ["plugins/markdown"],
|
|
||||||
"opts": {
|
|
||||||
"destination": "./out/",
|
|
||||||
"recurse": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,5 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "independent",
|
"version": "independent"
|
||||||
"npmClient": "pnpm",
|
|
||||||
"useWorkspaces": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
# my-patterns
|
|
||||||
|
|
||||||
This directory can be used to save your own patterns, which then get
|
|
||||||
made into a pattern swatch.
|
|
||||||
|
|
||||||
Example: <https://felixroos.github.io/strudel/swatch/>
|
|
||||||
|
|
||||||
## deploy
|
|
||||||
|
|
||||||
### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git)
|
|
||||||
|
|
||||||
### 2. clone your fork to your machine `git clone https://github.com/<your-username>/strudel.git strudel && cd strudel`
|
|
||||||
|
|
||||||
### 3. create a separate branch like `git branch patternuary && git checkout patternuary`
|
|
||||||
|
|
||||||
### 4. save one or more .txt files in the my-patterns folder
|
|
||||||
|
|
||||||
### 5. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
|
|
||||||
|
|
||||||
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
|
|
||||||
|
|
||||||
```js
|
|
||||||
const site = 'https://<your-username>.github.io';
|
|
||||||
const base = '/strudel';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. commit & push the changes
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git add . && git commit -m "site config" && git push --set-upstream origin
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. deploy to github pages
|
|
||||||
|
|
||||||
- go to settings -> pages and select "Github Actions" as source
|
|
||||||
- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches")
|
|
||||||
- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary`
|
|
||||||
|
|
||||||
### 9. view your patterns at `<your-username>.github.io/strudel/swatch/`
|
|
||||||
|
|
||||||
Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site).
|
|
||||||
|
|
||||||
### 10. optional: automatic deployment
|
|
||||||
|
|
||||||
If you want to automatically deploy your site on push, go to `deploy.yml` and change `workflow_dispatch` to `push`.
|
|
||||||
|
|
||||||
## running locally
|
|
||||||
|
|
||||||
- install dependencies with `npm run setup`
|
|
||||||
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
|
|
||||||
|
|
||||||
## tests fail?
|
|
||||||
|
|
||||||
Your tests might fail if the code does not follow prettiers format.
|
|
||||||
In that case, run `npm run codeformat`. To disable that, remove `npm run format-check` from `test.yml`
|
|
||||||
|
|
||||||
## updating your fork
|
|
||||||
|
|
||||||
To update your fork, you can pull the main branch and merge it into your `patternuary` branch.
|
|
||||||
@@ -1,32 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/monorepo",
|
"name": "@strudel.cycles/monorepo",
|
||||||
"version": "0.5.0",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Port of tidalcycles to javascript",
|
"description": "Port of tidalcycles to javascript",
|
||||||
|
"main": "strudel.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "pnpm i",
|
"test": "npm run test --workspaces --if-present",
|
||||||
"pretest": "npm run jsdoc-json",
|
"bootstrap": "lerna bootstrap"
|
||||||
"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",
|
|
||||||
"osc": "cd packages/osc && npm run server",
|
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -39,34 +23,14 @@
|
|||||||
"algorave"
|
"algorave"
|
||||||
],
|
],
|
||||||
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
|
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
|
||||||
"@strudel.cycles/xen": "workspace:*",
|
|
||||||
"acorn": "^8.8.1",
|
|
||||||
"dependency-tree": "^9.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
|
||||||
"@vitest/ui": "^0.28.0",
|
|
||||||
"canvas": "^2.11.2",
|
|
||||||
"eslint": "^8.39.0",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"jsdoc": "^4.0.2",
|
"lerna": "^4.0.0",
|
||||||
"jsdoc-json": "^2.0.2",
|
"mocha": "^9.1.4"
|
||||||
"jsdoc-to-markdown": "^8.0.0",
|
|
||||||
"lerna": "^6.6.1",
|
|
||||||
"prettier": "^2.8.8",
|
|
||||||
"rollup-plugin-visualizer": "^5.8.1",
|
|
||||||
"vitest": "^0.28.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# Packages
|
|
||||||
|
|
||||||
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
|
||||||
|
|
||||||
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
|
|
||||||
|
|
||||||
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Generated with
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
|
|
||||||
```
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -11,18 +11,18 @@ npm i @strudel.cycles/core --save
|
|||||||
## Example
|
## Example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { sequence } from '@strudel.cycles/core';
|
import { sequence, State, TimeSpan } from '@strudel.cycles/core';
|
||||||
|
|
||||||
const pattern = sequence('a', ['b', 'c']);
|
const pattern = sequence('a', ['b', 'c']);
|
||||||
|
|
||||||
const events = pattern.queryArc(0, 1);
|
const events = pattern.query(new State(new TimeSpan(0, 2)));
|
||||||
|
|
||||||
const spans = events.map(
|
const spans = events.map(
|
||||||
(event) => `${event.value}: ${event.whole.begin.toFraction()} - ${event.whole.end.toFraction()} `,
|
(event) => `${event.value}: ${event.whole.begin.toFraction()} - ${event.whole.end.toFraction()} `,
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
yields:
|
spans:
|
||||||
|
|
||||||
```log
|
```log
|
||||||
a: 0 - 1/2
|
a: 0 - 1/2
|
||||||
@@ -33,7 +33,4 @@ b: 3/2 - 7/4
|
|||||||
c: 7/4 - 2
|
c: 7/4 - 2
|
||||||
```
|
```
|
||||||
|
|
||||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
|
[play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
|
||||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
|
|
||||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
|
|
||||||
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
|
||||||
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
|
||||||
const { createParams } = controls;
|
|
||||||
|
|
||||||
let clearColor = '#22222210';
|
|
||||||
|
|
||||||
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
|
|
||||||
window.frame && cancelAnimationFrame(window.frame);
|
|
||||||
const ctx = getDrawContext();
|
|
||||||
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
|
|
||||||
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
|
|
||||||
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
|
|
||||||
clearColor = `#200010${smearPart}`;
|
|
||||||
const render = (t) => {
|
|
||||||
let frame;
|
|
||||||
/* if (sync) {
|
|
||||||
t = scheduler.now();
|
|
||||||
frame = this.queryArc(t, t);
|
|
||||||
} else { */
|
|
||||||
t = Math.round(t);
|
|
||||||
frame = this.slow(1000).queryArc(t, t);
|
|
||||||
// }
|
|
||||||
ctx.fillStyle = clearColor;
|
|
||||||
ctx.fillRect(0, 0, ww, wh);
|
|
||||||
frame.forEach((f) => {
|
|
||||||
let { x, y, w, h, s, r, angle = 0, fill = 'darkseagreen' } = f.value;
|
|
||||||
w *= ww;
|
|
||||||
h *= wh;
|
|
||||||
if (r !== undefined && angle !== undefined) {
|
|
||||||
const radians = angle * 2 * Math.PI;
|
|
||||||
const [cx, cy] = [(ww - w) / 2, (wh - h) / 2];
|
|
||||||
x = cx + Math.cos(radians) * r * cx;
|
|
||||||
y = cy + Math.sin(radians) * r * cy;
|
|
||||||
} else {
|
|
||||||
x *= ww - w;
|
|
||||||
y *= wh - h;
|
|
||||||
}
|
|
||||||
const val = { ...f.value, x, y, w, h };
|
|
||||||
ctx.fillStyle = fill;
|
|
||||||
if (s === 'rect') {
|
|
||||||
ctx.fillRect(x, y, w, h);
|
|
||||||
} else if (s === 'ellipse') {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
callback && callback(ctx, val, f);
|
|
||||||
});
|
|
||||||
window.frame = requestAnimationFrame(render);
|
|
||||||
};
|
|
||||||
window.frame = requestAnimationFrame(render);
|
|
||||||
return silence;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'fill', 'smear');
|
|
||||||
|
|
||||||
export const rescale = register('rescale', function (f, pat) {
|
|
||||||
return pat.mul(x(f).w(f).y(f).h(f));
|
|
||||||
});
|
|
||||||
|
|
||||||
export const moveXY = register('moveXY', function (dx, dy, pat) {
|
|
||||||
return pat.add(x(dx).y(dy));
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zoomIn = register('zoomIn', function (f, pat) {
|
|
||||||
const d = pure(1).sub(f).div(2);
|
|
||||||
return pat.rescale(f).move(d, d);
|
|
||||||
});
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
export const colorMap = {
|
|
||||||
aliceblue: '#f0f8ff',
|
|
||||||
antiquewhite: '#faebd7',
|
|
||||||
aqua: '#00ffff',
|
|
||||||
aquamarine: '#7fffd4',
|
|
||||||
azure: '#f0ffff',
|
|
||||||
beige: '#f5f5dc',
|
|
||||||
bisque: '#ffe4c4',
|
|
||||||
black: '#000000',
|
|
||||||
blanchedalmond: '#ffebcd',
|
|
||||||
blue: '#0000ff',
|
|
||||||
blueviolet: '#8a2be2',
|
|
||||||
brown: '#a52a2a',
|
|
||||||
burlywood: '#deb887',
|
|
||||||
cadetblue: '#5f9ea0',
|
|
||||||
chartreuse: '#7fff00',
|
|
||||||
chocolate: '#d2691e',
|
|
||||||
coral: '#ff7f50',
|
|
||||||
cornflowerblue: '#6495ed',
|
|
||||||
cornsilk: '#fff8dc',
|
|
||||||
crimson: '#dc143c',
|
|
||||||
cyan: '#00ffff',
|
|
||||||
darkblue: '#00008b',
|
|
||||||
darkcyan: '#008b8b',
|
|
||||||
darkgoldenrod: '#b8860b',
|
|
||||||
darkgray: '#a9a9a9',
|
|
||||||
darkgreen: '#006400',
|
|
||||||
darkgrey: '#a9a9a9',
|
|
||||||
darkkhaki: '#bdb76b',
|
|
||||||
darkmagenta: '#8b008b',
|
|
||||||
darkolivegreen: '#556b2f',
|
|
||||||
darkorange: '#ff8c00',
|
|
||||||
darkorchid: '#9932cc',
|
|
||||||
darkred: '#8b0000',
|
|
||||||
darksalmon: '#e9967a',
|
|
||||||
darkseagreen: '#8fbc8f',
|
|
||||||
darkslateblue: '#483d8b',
|
|
||||||
darkslategray: '#2f4f4f',
|
|
||||||
darkslategrey: '#2f4f4f',
|
|
||||||
darkturquoise: '#00ced1',
|
|
||||||
darkviolet: '#9400d3',
|
|
||||||
deeppink: '#ff1493',
|
|
||||||
deepskyblue: '#00bfff',
|
|
||||||
dimgray: '#696969',
|
|
||||||
dimgrey: '#696969',
|
|
||||||
dodgerblue: '#1e90ff',
|
|
||||||
firebrick: '#b22222',
|
|
||||||
floralwhite: '#fffaf0',
|
|
||||||
forestgreen: '#228b22',
|
|
||||||
fuchsia: '#ff00ff',
|
|
||||||
gainsboro: '#dcdcdc',
|
|
||||||
ghostwhite: '#f8f8ff',
|
|
||||||
gold: '#ffd700',
|
|
||||||
goldenrod: '#daa520',
|
|
||||||
gray: '#808080',
|
|
||||||
green: '#008000',
|
|
||||||
greenyellow: '#adff2f',
|
|
||||||
grey: '#808080',
|
|
||||||
honeydew: '#f0fff0',
|
|
||||||
hotpink: '#ff69b4',
|
|
||||||
indianred: '#cd5c5c',
|
|
||||||
indigo: '#4b0082',
|
|
||||||
ivory: '#fffff0',
|
|
||||||
khaki: '#f0e68c',
|
|
||||||
lavender: '#e6e6fa',
|
|
||||||
lavenderblush: '#fff0f5',
|
|
||||||
lawngreen: '#7cfc00',
|
|
||||||
lemonchiffon: '#fffacd',
|
|
||||||
lightblue: '#add8e6',
|
|
||||||
lightcoral: '#f08080',
|
|
||||||
lightcyan: '#e0ffff',
|
|
||||||
lightgoldenrodyellow: '#fafad2',
|
|
||||||
lightgray: '#d3d3d3',
|
|
||||||
lightgreen: '#90ee90',
|
|
||||||
lightgrey: '#d3d3d3',
|
|
||||||
lightpink: '#ffb6c1',
|
|
||||||
lightsalmon: '#ffa07a',
|
|
||||||
lightseagreen: '#20b2aa',
|
|
||||||
lightskyblue: '#87cefa',
|
|
||||||
lightslategray: '#778899',
|
|
||||||
lightslategrey: '#778899',
|
|
||||||
lightsteelblue: '#b0c4de',
|
|
||||||
lightyellow: '#ffffe0',
|
|
||||||
lime: '#00ff00',
|
|
||||||
limegreen: '#32cd32',
|
|
||||||
linen: '#faf0e6',
|
|
||||||
magenta: '#ff00ff',
|
|
||||||
maroon: '#800000',
|
|
||||||
mediumaquamarine: '#66cdaa',
|
|
||||||
mediumblue: '#0000cd',
|
|
||||||
mediumorchid: '#ba55d3',
|
|
||||||
mediumpurple: '#9370db',
|
|
||||||
mediumseagreen: '#3cb371',
|
|
||||||
mediumslateblue: '#7b68ee',
|
|
||||||
mediumspringgreen: '#00fa9a',
|
|
||||||
mediumturquoise: '#48d1cc',
|
|
||||||
mediumvioletred: '#c71585',
|
|
||||||
midnightblue: '#191970',
|
|
||||||
mintcream: '#f5fffa',
|
|
||||||
mistyrose: '#ffe4e1',
|
|
||||||
moccasin: '#ffe4b5',
|
|
||||||
navajowhite: '#ffdead',
|
|
||||||
navy: '#000080',
|
|
||||||
oldlace: '#fdf5e6',
|
|
||||||
olive: '#808000',
|
|
||||||
olivedrab: '#6b8e23',
|
|
||||||
orange: '#ffa500',
|
|
||||||
orangered: '#ff4500',
|
|
||||||
orchid: '#da70d6',
|
|
||||||
palegoldenrod: '#eee8aa',
|
|
||||||
palegreen: '#98fb98',
|
|
||||||
paleturquoise: '#afeeee',
|
|
||||||
palevioletred: '#db7093',
|
|
||||||
papayawhip: '#ffefd5',
|
|
||||||
peachpuff: '#ffdab9',
|
|
||||||
peru: '#cd853f',
|
|
||||||
pink: '#ffc0cb',
|
|
||||||
plum: '#dda0dd',
|
|
||||||
powderblue: '#b0e0e6',
|
|
||||||
purple: '#800080',
|
|
||||||
red: '#ff0000',
|
|
||||||
rosybrown: '#bc8f8f',
|
|
||||||
royalblue: '#4169e1',
|
|
||||||
saddlebrown: '#8b4513',
|
|
||||||
salmon: '#fa8072',
|
|
||||||
sandybrown: '#f4a460',
|
|
||||||
seagreen: '#2e8b57',
|
|
||||||
seashell: '#fff5ee',
|
|
||||||
sienna: '#a0522d',
|
|
||||||
silver: '#c0c0c0',
|
|
||||||
skyblue: '#87ceeb',
|
|
||||||
slateblue: '#6a5acd',
|
|
||||||
slategray: '#708090',
|
|
||||||
slategrey: '#708090',
|
|
||||||
snow: '#fffafa',
|
|
||||||
springgreen: '#00ff7f',
|
|
||||||
steelblue: '#4682b4',
|
|
||||||
tan: '#d2b48c',
|
|
||||||
teal: '#008080',
|
|
||||||
thistle: '#d8bfd8',
|
|
||||||
tomato: '#ff6347',
|
|
||||||
turquoise: '#40e0d0',
|
|
||||||
violet: '#ee82ee',
|
|
||||||
wheat: '#f5deb3',
|
|
||||||
white: '#ffffff',
|
|
||||||
whitesmoke: '#f5f5f5',
|
|
||||||
yellow: '#ffff00',
|
|
||||||
yellowgreen: '#9acd32',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function convertColorToNumber(color) {
|
|
||||||
// Convert color to lowercase for easier matching
|
|
||||||
color = color.toLowerCase();
|
|
||||||
|
|
||||||
// If the color is a hex code, convert it to a number
|
|
||||||
if (color[0] === '#') {
|
|
||||||
return convertHexToNumber(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the color is a named color, return the corresponding number
|
|
||||||
if (colorMap[color] !== undefined) {
|
|
||||||
return convertHexToNumber(colorMap[color]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the color is not recognized, return null
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertHexToNumber(hex) {
|
|
||||||
// Remove the leading '#' from the hex code
|
|
||||||
hex = hex.slice(1);
|
|
||||||
|
|
||||||
// Convert the hex code to a number
|
|
||||||
return parseInt(hex, 16);
|
|
||||||
}
|
|
||||||
@@ -1,826 +0,0 @@
|
|||||||
/*
|
|
||||||
controls.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.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, register, sequence } from './pattern.mjs';
|
|
||||||
import { zipWith } from './util.mjs';
|
|
||||||
|
|
||||||
const controls = {};
|
|
||||||
const generic_params = [
|
|
||||||
/**
|
|
||||||
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
|
||||||
* separated by ':'.
|
|
||||||
*
|
|
||||||
* @name s
|
|
||||||
* @param {string | Pattern} sound The sound / pattern of sounds to pick
|
|
||||||
* @synonyms sound
|
|
||||||
* @example
|
|
||||||
* s("bd hh")
|
|
||||||
* @example
|
|
||||||
* s("bd:0 bd:1 bd:0:0.3 bd:1:1.4")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
[['s', 'n', 'gain'], 'sound'],
|
|
||||||
/**
|
|
||||||
* Define a custom webaudio node to use as a sound source.
|
|
||||||
*
|
|
||||||
* @name source
|
|
||||||
* @param {function} getSource
|
|
||||||
* @synonyms src
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['source', 'src'],
|
|
||||||
/**
|
|
||||||
* Selects the given index from the sample map.
|
|
||||||
* Numbers too high will wrap around.
|
|
||||||
* `n` can also be used to play midi numbers, but it is recommended to use `note` instead.
|
|
||||||
*
|
|
||||||
* @name n
|
|
||||||
* @param {number | Pattern} value sample index starting from 0
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*3").n("<0 1>")
|
|
||||||
*/
|
|
||||||
// also see https://github.com/tidalcycles/strudel/pull/63
|
|
||||||
['n'],
|
|
||||||
/**
|
|
||||||
* Plays the given note name or midi number. A note name consists of
|
|
||||||
*
|
|
||||||
* - a letter (a-g or A-G)
|
|
||||||
* - optional accidentals (b or #)
|
|
||||||
* - optional octave number (0-9). Defaults to 3
|
|
||||||
*
|
|
||||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
|
||||||
*
|
|
||||||
* You can also use midi numbers instead of note names, where 69 is mapped to A4 440Hz in 12EDO.
|
|
||||||
*
|
|
||||||
* @name note
|
|
||||||
* @example
|
|
||||||
* note("c a f e")
|
|
||||||
* @example
|
|
||||||
* note("c4 a4 f4 e4")
|
|
||||||
* @example
|
|
||||||
* note("60 69 65 64")
|
|
||||||
*/
|
|
||||||
[['note', 'n']],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A pattern of numbers that speed up (or slow down) samples while they play. Currently only supported by osc / superdirt.
|
|
||||||
*
|
|
||||||
* @name accelerate
|
|
||||||
* @param {number | Pattern} amount acceleration.
|
|
||||||
* @superdirtOnly
|
|
||||||
* @example
|
|
||||||
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['accelerate'],
|
|
||||||
/**
|
|
||||||
* Controls the gain by an exponential amount.
|
|
||||||
*
|
|
||||||
* @name gain
|
|
||||||
* @param {number | Pattern} amount gain.
|
|
||||||
* @example
|
|
||||||
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['gain'],
|
|
||||||
/**
|
|
||||||
* Like {@link gain}, but linear.
|
|
||||||
*
|
|
||||||
* @name amp
|
|
||||||
* @param {number | Pattern} amount gain.
|
|
||||||
* @superdirtOnly
|
|
||||||
* @example
|
|
||||||
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['amp'],
|
|
||||||
/**
|
|
||||||
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
|
|
||||||
*
|
|
||||||
* @name attack
|
|
||||||
* @param {number | Pattern} attack time in seconds.
|
|
||||||
* @synonyms att
|
|
||||||
* @example
|
|
||||||
* note("c3 e3").attack("<0 .1 .5>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['attack', 'att'],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
|
||||||
*
|
|
||||||
* @name bank
|
|
||||||
* @param {string | Pattern} bank the name of the bank
|
|
||||||
* @example
|
|
||||||
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['bank'],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
|
|
||||||
* Note that the decay is only audible if the sustain value is lower than 1.
|
|
||||||
*
|
|
||||||
* @name decay
|
|
||||||
* @param {number | Pattern} time decay time in seconds
|
|
||||||
* @example
|
|
||||||
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['decay'],
|
|
||||||
/**
|
|
||||||
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
|
|
||||||
*
|
|
||||||
* @name sustain
|
|
||||||
* @param {number | Pattern} gain sustain level between 0 and 1
|
|
||||||
* @synonyms sus
|
|
||||||
* @example
|
|
||||||
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['sustain', 'sus'],
|
|
||||||
/**
|
|
||||||
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
|
|
||||||
*
|
|
||||||
* @name release
|
|
||||||
* @param {number | Pattern} time release time in seconds
|
|
||||||
* @synonyms rel
|
|
||||||
* @example
|
|
||||||
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['release', 'rel'],
|
|
||||||
['hold'],
|
|
||||||
// TODO: in tidal, it seems to be normalized
|
|
||||||
/**
|
|
||||||
* Sets the center frequency of the **b**and-**p**ass **f**ilter. When using mininotation, you
|
|
||||||
* can also optionally supply the 'bpq' parameter separated by ':'.
|
|
||||||
*
|
|
||||||
* @name bpf
|
|
||||||
* @param {number | Pattern} frequency center frequency
|
|
||||||
* @synonyms bandf, bp
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
[['bandf', 'bandq'], 'bpf', 'bp'],
|
|
||||||
// TODO: in tidal, it seems to be normalized
|
|
||||||
/**
|
|
||||||
* Sets the **b**and-**p**ass **q**-factor (resonance).
|
|
||||||
*
|
|
||||||
* @name bpq
|
|
||||||
* @param {number | Pattern} q q factor
|
|
||||||
* @synonyms bandq
|
|
||||||
* @example
|
|
||||||
* s("bd sd").bpf(500).bpq("<0 1 2 3>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496
|
|
||||||
// ['bpq'],
|
|
||||||
['bandq', 'bpq'],
|
|
||||||
/**
|
|
||||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
|
||||||
*
|
|
||||||
* @memberof Pattern
|
|
||||||
* @name begin
|
|
||||||
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
|
|
||||||
* @example
|
|
||||||
* samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/')
|
|
||||||
* s("rave").begin("<0 .25 .5 .75>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['begin'],
|
|
||||||
/**
|
|
||||||
* The same as .begin, but cuts off the end off each sample.
|
|
||||||
*
|
|
||||||
* @memberof Pattern
|
|
||||||
* @name end
|
|
||||||
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
|
|
||||||
* @example
|
|
||||||
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['end'],
|
|
||||||
/**
|
|
||||||
* Loops the sample (from `begin` to `end`) the specified number of times.
|
|
||||||
* Note that the tempo of the loop is not synced with the cycle tempo.
|
|
||||||
*
|
|
||||||
* @name loop
|
|
||||||
* @param {number | Pattern} times How often the sample is looped
|
|
||||||
* @example
|
|
||||||
* s("bd").loop("<1 2 3 4>").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['loop'],
|
|
||||||
// TODO: currently duplicated with "native" legato
|
|
||||||
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
|
|
||||||
/**
|
|
||||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
|
||||||
*
|
|
||||||
* @name legato
|
|
||||||
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
|
|
||||||
* @noAutocomplete
|
|
||||||
* @example
|
|
||||||
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// ['legato'],
|
|
||||||
// ['clhatdecay'],
|
|
||||||
/**
|
|
||||||
* bit crusher effect.
|
|
||||||
*
|
|
||||||
* @name crush
|
|
||||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
|
||||||
* @example
|
|
||||||
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['crush'],
|
|
||||||
/**
|
|
||||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
|
||||||
*
|
|
||||||
* @name coarse
|
|
||||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['coarse'],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* choose the channel the pattern is sent to in superdirt
|
|
||||||
*
|
|
||||||
* @name channel
|
|
||||||
* @param {number | Pattern} channel channel number
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['channel'],
|
|
||||||
/**
|
|
||||||
* In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.
|
|
||||||
*
|
|
||||||
* @name cut
|
|
||||||
* @param {number | Pattern} group cut group number
|
|
||||||
* @example
|
|
||||||
* s("rd*4").cut(1)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['cut'],
|
|
||||||
/**
|
|
||||||
* Applies the cutoff frequency of the **l**ow-**p**ass **f**ilter.
|
|
||||||
*
|
|
||||||
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
|
|
||||||
*
|
|
||||||
* @name lpf
|
|
||||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
|
||||||
* @synonyms cutoff, ctf, lp
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*3").lpf("<4000 2000 1000 500 200 100>")
|
|
||||||
* @example
|
|
||||||
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
|
|
||||||
/**
|
|
||||||
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
|
|
||||||
*
|
|
||||||
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
|
|
||||||
*
|
|
||||||
* @name hpf
|
|
||||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
|
||||||
* @synonyms hp, hcutoff
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*4").hpf("<4000 2000 1000 500 200 100>")
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*4").hpf("<2000 2000:25>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
|
|
||||||
// ['hpf'],
|
|
||||||
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
|
|
||||||
/**
|
|
||||||
* Controls the **h**igh-**p**ass **q**-value.
|
|
||||||
*
|
|
||||||
* @name hpq
|
|
||||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
|
||||||
* @synonyms hresonance
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*4").hpf(2000).hpq("<0 10 20 30>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['hresonance', 'hpq'],
|
|
||||||
/**
|
|
||||||
* Controls the **l**ow-**p**ass **q**-value.
|
|
||||||
*
|
|
||||||
* @name lpq
|
|
||||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
|
||||||
* @synonyms resonance
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*4").lpf(2000).lpq("<0 10 20 30>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496
|
|
||||||
['resonance', 'lpq'],
|
|
||||||
/**
|
|
||||||
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
|
|
||||||
*
|
|
||||||
* @name djf
|
|
||||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
|
||||||
* @example
|
|
||||||
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['djf'],
|
|
||||||
// ['cutoffegint'],
|
|
||||||
// TODO: does not seem to work
|
|
||||||
/**
|
|
||||||
* Sets the level of the delay signal.
|
|
||||||
*
|
|
||||||
* When using mininotation, you can also optionally add the 'delaytime' and 'delayfeedback' parameter,
|
|
||||||
* separated by ':'.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @name delay
|
|
||||||
* @param {number | Pattern} level between 0 and 1
|
|
||||||
* @example
|
|
||||||
* s("bd").delay("<0 .25 .5 1>")
|
|
||||||
* @example
|
|
||||||
* s("bd bd").delay("0.65:0.25:0.9 0.65:0.125:0.7")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
[['delay', 'delaytime', 'delayfeedback']],
|
|
||||||
/**
|
|
||||||
* Sets the level of the signal that is fed back into the delay.
|
|
||||||
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
|
||||||
*
|
|
||||||
* @name delayfeedback
|
|
||||||
* @param {number | Pattern} feedback between 0 and 1
|
|
||||||
* @synonyms delayfb, dfb
|
|
||||||
* @example
|
|
||||||
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['delayfeedback', 'delayfb', 'dfb'],
|
|
||||||
/**
|
|
||||||
* Sets the time of the delay effect.
|
|
||||||
*
|
|
||||||
* @name delaytime
|
|
||||||
* @param {number | Pattern} seconds between 0 and Infinity
|
|
||||||
* @synonyms delayt, dt
|
|
||||||
* @example
|
|
||||||
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['delaytime', 'delayt', 'dt'],
|
|
||||||
/* // TODO: test
|
|
||||||
* Specifies whether delaytime is calculated relative to cps.
|
|
||||||
*
|
|
||||||
* @name lock
|
|
||||||
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
|
|
||||||
* @example
|
|
||||||
* s("sd").delay().lock(1).osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['lock'],
|
|
||||||
/**
|
|
||||||
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
|
|
||||||
*
|
|
||||||
* @name detune
|
|
||||||
* @param {number | Pattern} amount between 0 and 1
|
|
||||||
* @synonyms det
|
|
||||||
* @superdirtOnly
|
|
||||||
* @example
|
|
||||||
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['detune', 'det'],
|
|
||||||
/**
|
|
||||||
* Set dryness of reverb. See {@link room} and {@link size} for more information about reverb.
|
|
||||||
*
|
|
||||||
* @name dry
|
|
||||||
* @param {number | Pattern} dry 0 = wet, 1 = dry
|
|
||||||
* @example
|
|
||||||
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
|
|
||||||
* @superdirtOnly
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['dry'],
|
|
||||||
// TODO: does not seem to do anything
|
|
||||||
/*
|
|
||||||
* Used when using {@link begin}/{@link end} or {@link chop}/{@link striate} and friends, to change the fade out time of the 'grain' envelope.
|
|
||||||
*
|
|
||||||
* @name fadeTime
|
|
||||||
* @param {number | Pattern} time between 0 and 1
|
|
||||||
* @example
|
|
||||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['fadeTime', 'fadeOutTime'],
|
|
||||||
// TODO: see above
|
|
||||||
['fadeInTime'],
|
|
||||||
/**
|
|
||||||
* Set frequency of sound.
|
|
||||||
*
|
|
||||||
* @name freq
|
|
||||||
* @param {number | Pattern} frequency in Hz. the audible range is between 20 and 20000 Hz
|
|
||||||
* @example
|
|
||||||
* freq("220 110 440 110").s("superzow").osc()
|
|
||||||
* @example
|
|
||||||
* freq("110".mul.out(".5 1.5 .6 [2 3]")).s("superzow").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['freq'],
|
|
||||||
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
|
|
||||||
['gate', 'gat'],
|
|
||||||
// ['hatgrain'],
|
|
||||||
// ['lagogo'],
|
|
||||||
// ['lclap'],
|
|
||||||
// ['lclaves'],
|
|
||||||
// ['lclhat'],
|
|
||||||
// ['lcrash'],
|
|
||||||
// TODO:
|
|
||||||
// https://tidalcycles.org/docs/reference/audio_effects/#leslie-1
|
|
||||||
// https://tidalcycles.org/docs/reference/audio_effects/#leslie
|
|
||||||
/**
|
|
||||||
* Emulation of a Leslie speaker: speakers rotating in a wooden amplified cabinet.
|
|
||||||
*
|
|
||||||
* @name leslie
|
|
||||||
* @param {number | Pattern} wet between 0 and 1
|
|
||||||
* @example
|
|
||||||
* n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc()
|
|
||||||
* @superdirtOnly
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['leslie'],
|
|
||||||
/**
|
|
||||||
* Rate of modulation / rotation for leslie effect
|
|
||||||
*
|
|
||||||
* @name lrate
|
|
||||||
* @param {number | Pattern} rate 6.7 for fast, 0.7 for slow
|
|
||||||
* @example
|
|
||||||
* n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc()
|
|
||||||
* @superdirtOnly
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// TODO: the rate seems to "lag" (in the example, 1 will be fast)
|
|
||||||
['lrate'],
|
|
||||||
/**
|
|
||||||
* Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble)
|
|
||||||
*
|
|
||||||
* @name lsize
|
|
||||||
* @param {number | Pattern} meters somewhere between 0 and 1
|
|
||||||
* @example
|
|
||||||
* n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc()
|
|
||||||
* @superdirtOnly
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['lsize'],
|
|
||||||
// ['lfo'],
|
|
||||||
// ['lfocutoffint'],
|
|
||||||
// ['lfodelay'],
|
|
||||||
// ['lfoint'],
|
|
||||||
// ['lfopitchint'],
|
|
||||||
// ['lfoshape'],
|
|
||||||
// ['lfosync'],
|
|
||||||
// ['lhitom'],
|
|
||||||
// ['lkick'],
|
|
||||||
// ['llotom'],
|
|
||||||
// ['lophat'],
|
|
||||||
// ['lsnare'],
|
|
||||||
['degree'], // TODO: what is this? not found in tidal doc
|
|
||||||
['mtranspose'], // TODO: what is this? not found in tidal doc
|
|
||||||
['ctranspose'], // TODO: what is this? not found in tidal doc
|
|
||||||
['harmonic'], // TODO: what is this? not found in tidal doc
|
|
||||||
['stepsPerOctave'], // TODO: what is this? not found in tidal doc
|
|
||||||
['octaveR'], // TODO: what is this? not found in tidal doc
|
|
||||||
// TODO: why is this needed? what's the difference to late / early? Answer: it's in seconds, and delays the message at
|
|
||||||
// OSC time (so can't be negative, at least not beyond the latency value)
|
|
||||||
['nudge'],
|
|
||||||
// TODO: the following doc is just a guess, it's not documented in tidal doc.
|
|
||||||
/**
|
|
||||||
* Sets the default octave of a synth.
|
|
||||||
*
|
|
||||||
* @name octave
|
|
||||||
* @param {number | Pattern} octave octave number
|
|
||||||
* @example
|
|
||||||
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
|
|
||||||
* @superDirtOnly
|
|
||||||
*/
|
|
||||||
['octave'],
|
|
||||||
['offset'], // TODO: what is this? not found in tidal doc
|
|
||||||
// ['ophatdecay'],
|
|
||||||
// TODO: example
|
|
||||||
/**
|
|
||||||
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
|
|
||||||
*
|
|
||||||
* @name orbit
|
|
||||||
* @param {number | Pattern} number
|
|
||||||
* @example
|
|
||||||
* stack(
|
|
||||||
* s("hh*3").delay(.5).delaytime(.25).orbit(1),
|
|
||||||
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
|
|
||||||
* )
|
|
||||||
*/
|
|
||||||
['orbit'],
|
|
||||||
['overgain'], // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
|
||||||
['overshape'], // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
|
||||||
/**
|
|
||||||
* Sets position in stereo.
|
|
||||||
*
|
|
||||||
* @name pan
|
|
||||||
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
|
|
||||||
* @example
|
|
||||||
* s("[bd hh]*2").pan("<.5 1 .5 0>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['pan'],
|
|
||||||
// TODO: this has no effect (see example)
|
|
||||||
/*
|
|
||||||
* Controls how much multichannel output is fanned out
|
|
||||||
*
|
|
||||||
* @name panspan
|
|
||||||
* @param {number | Pattern} span between -inf and inf, negative is backwards ordering
|
|
||||||
* @example
|
|
||||||
* s("[bd hh]*2").pan("<.5 1 .5 0>").panspan("<0 .5 1>").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['panspan'],
|
|
||||||
// TODO: this has no effect (see example)
|
|
||||||
/*
|
|
||||||
* Controls how much multichannel output is spread
|
|
||||||
*
|
|
||||||
* @name pansplay
|
|
||||||
* @param {number | Pattern} spread between 0 and 1
|
|
||||||
* @example
|
|
||||||
* s("[bd hh]*2").pan("<.5 1 .5 0>").pansplay("<0 .5 1>").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['pansplay'],
|
|
||||||
['panwidth'],
|
|
||||||
['panorient'],
|
|
||||||
// ['pitch1'],
|
|
||||||
// ['pitch2'],
|
|
||||||
// ['pitch3'],
|
|
||||||
// ['portamento'],
|
|
||||||
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
|
||||||
['rate'],
|
|
||||||
// TODO: slide param for certain synths
|
|
||||||
['slide'],
|
|
||||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
|
||||||
['semitone'],
|
|
||||||
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
|
|
||||||
// ['velocity'],
|
|
||||||
['voice'], // TODO: synth param
|
|
||||||
/**
|
|
||||||
* Sets the level of reverb.
|
|
||||||
*
|
|
||||||
* When using mininotation, you can also optionally add the 'size' parameter, separated by ':'.
|
|
||||||
*
|
|
||||||
* @name room
|
|
||||||
* @param {number | Pattern} level between 0 and 1
|
|
||||||
* @example
|
|
||||||
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
|
|
||||||
* @example
|
|
||||||
* s("bd sd").room("<0.9:1 0.9:4>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
[['room', 'size']],
|
|
||||||
/**
|
|
||||||
* Sets the room size of the reverb, see {@link room}.
|
|
||||||
*
|
|
||||||
* @name roomsize
|
|
||||||
* @param {number | Pattern} size between 0 and 10
|
|
||||||
* @synonyms size, sz
|
|
||||||
* @example
|
|
||||||
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// TODO: find out why :
|
|
||||||
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
|
|
||||||
// .. does not work. Is it because room is only one effect?
|
|
||||||
['size', 'sz', 'roomsize'],
|
|
||||||
// ['sagogo'],
|
|
||||||
// ['sclap'],
|
|
||||||
// ['sclaves'],
|
|
||||||
// ['scrash'],
|
|
||||||
/**
|
|
||||||
* Wave shaping distortion. CAUTION: it might get loud
|
|
||||||
*
|
|
||||||
* @name shape
|
|
||||||
* @param {number | Pattern} distortion between 0 and 1
|
|
||||||
* @example
|
|
||||||
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['shape'],
|
|
||||||
/**
|
|
||||||
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
|
|
||||||
*
|
|
||||||
* @name speed
|
|
||||||
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
|
|
||||||
* @example
|
|
||||||
* s("bd").speed("<1 2 4 1 -2 -4>")
|
|
||||||
* @example
|
|
||||||
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['speed'],
|
|
||||||
/**
|
|
||||||
* Used in conjunction with {@link speed}, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
|
|
||||||
*
|
|
||||||
* @name unit
|
|
||||||
* @param {number | string | Pattern} unit see description above
|
|
||||||
* @example
|
|
||||||
* speed("1 2 .5 3").s("bd").unit("c").osc()
|
|
||||||
* @superdirtOnly
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['unit'],
|
|
||||||
/**
|
|
||||||
* Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as:
|
|
||||||
*
|
|
||||||
* "A simplistic pitch-raising algorithm. It's not meant to sound natural; its sound is reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter, depending on the input. The algorithm works by cutting the signal into fragments (delimited by upwards-going zero-crossings) and squeezing those fragments in the time domain (i.e. simply playing them back faster than they came in), leaving silences inbetween. All the parameters apart from memlen can be modulated."
|
|
||||||
*
|
|
||||||
* @name squiz
|
|
||||||
* @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc.
|
|
||||||
* @example
|
|
||||||
* squiz("2 4/2 6 [8 16]").s("bd").osc()
|
|
||||||
* @superdirtOnly
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['squiz'],
|
|
||||||
// ['stutterdepth'], // TODO: what is this? not found in tidal doc
|
|
||||||
// ['stuttertime'], // TODO: what is this? not found in tidal doc
|
|
||||||
// ['timescale'], // TODO: what is this? not found in tidal doc
|
|
||||||
// ['timescalewin'], // TODO: what is this? not found in tidal doc
|
|
||||||
// ['tomdecay'],
|
|
||||||
// ['vcfegint'],
|
|
||||||
// ['vcoegint'],
|
|
||||||
// TODO: Use a rest (~) to override the effect <- vowel
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Formant filter to make things sound like vowels.
|
|
||||||
*
|
|
||||||
* @name vowel
|
|
||||||
* @param {string | Pattern} vowel You can use a e i o u.
|
|
||||||
* @example
|
|
||||||
* note("c2 <eb2 <g2 g1>>").s('sawtooth')
|
|
||||||
* .vowel("<a e i <o u>>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['vowel'],
|
|
||||||
/* // TODO: find out how it works
|
|
||||||
* Made by Calum Gunn. Divides an audio stream into tiny segments, using the signal's zero-crossings as segment boundaries, and discards a fraction of them. Takes a number between 1 and 100, denoted the percentage of segments to drop. The SuperCollider manual describes the Waveloss effect this way:
|
|
||||||
*
|
|
||||||
* Divide an audio stream into tiny segments, using the signal's zero-crossings as segment boundaries, and discard a fraction of them (i.e. replace them with silence of the same length). The technique was described by Trevor Wishart in a lecture. Parameters: the filter drops drop out of out of chunks. mode can be 1 to drop chunks in a simple deterministic fashion (e.g. always dropping the first 30 out of a set of 40 segments), or 2 to drop chunks randomly but in an appropriate proportion.)
|
|
||||||
*
|
|
||||||
* mode: ?
|
|
||||||
* waveloss: ?
|
|
||||||
*
|
|
||||||
* @name waveloss
|
|
||||||
*/
|
|
||||||
['waveloss'],
|
|
||||||
// TODO: midi effects?
|
|
||||||
['dur'],
|
|
||||||
// ['modwheel'],
|
|
||||||
['expression'],
|
|
||||||
['sustainpedal'],
|
|
||||||
/* // TODO: doesn't seem to do anything
|
|
||||||
*
|
|
||||||
* Tremolo Audio DSP effect
|
|
||||||
*
|
|
||||||
* @name tremolodepth
|
|
||||||
* @param {number | Pattern} depth between 0 and 1
|
|
||||||
* @example
|
|
||||||
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['tremolodepth', 'tremdp'],
|
|
||||||
['tremolorate', 'tremr'],
|
|
||||||
// TODO: doesn't seem to do anything
|
|
||||||
['phaserdepth', 'phasdp'],
|
|
||||||
['phaserrate', 'phasr'],
|
|
||||||
|
|
||||||
['fshift'],
|
|
||||||
['fshiftnote'],
|
|
||||||
['fshiftphase'],
|
|
||||||
|
|
||||||
['triode'],
|
|
||||||
['krush'],
|
|
||||||
['kcutoff'],
|
|
||||||
['octer'],
|
|
||||||
['octersub'],
|
|
||||||
['octersubsub'],
|
|
||||||
['ring'],
|
|
||||||
['ringf'],
|
|
||||||
['ringdf'],
|
|
||||||
['distort'],
|
|
||||||
['freeze'],
|
|
||||||
['xsdelay'],
|
|
||||||
['tsdelay'],
|
|
||||||
['real'],
|
|
||||||
['imag'],
|
|
||||||
['enhance'],
|
|
||||||
['partials'],
|
|
||||||
['comb'],
|
|
||||||
['smear'],
|
|
||||||
['scram'],
|
|
||||||
['binshift'],
|
|
||||||
['hbrick'],
|
|
||||||
['lbrick'],
|
|
||||||
['midichan'],
|
|
||||||
['control'],
|
|
||||||
['ccn'],
|
|
||||||
['ccv'],
|
|
||||||
['polyTouch'],
|
|
||||||
['midibend'],
|
|
||||||
['miditouch'],
|
|
||||||
['ctlNum'],
|
|
||||||
['frameRate'],
|
|
||||||
['frames'],
|
|
||||||
['hours'],
|
|
||||||
['midicmd'],
|
|
||||||
['minutes'],
|
|
||||||
['progNum'],
|
|
||||||
['seconds'],
|
|
||||||
['songPtr'],
|
|
||||||
['uid'],
|
|
||||||
['val'],
|
|
||||||
['cps'],
|
|
||||||
/**
|
|
||||||
* Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration.
|
|
||||||
* In tidal, this would be done with legato, [which has a complicated history in strudel](https://github.com/tidalcycles/strudel/issues/111).
|
|
||||||
* For now, if you're coming from tidal, just think clip = legato.
|
|
||||||
*
|
|
||||||
* @name clip
|
|
||||||
* @param {number | Pattern} factor >= 0
|
|
||||||
* @example
|
|
||||||
* note("c a f e").s("piano").clip("<.5 1 2>")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
['clip'],
|
|
||||||
];
|
|
||||||
|
|
||||||
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
|
|
||||||
|
|
||||||
controls.createParam = function (names) {
|
|
||||||
const name = Array.isArray(names) ? names[0] : names;
|
|
||||||
|
|
||||||
var withVal;
|
|
||||||
if (Array.isArray(names)) {
|
|
||||||
withVal = (xs) => {
|
|
||||||
if (Array.isArray(xs)) {
|
|
||||||
const result = {};
|
|
||||||
xs.forEach((x, i) => {
|
|
||||||
if (i < names.length) {
|
|
||||||
result[names[i]] = x;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
return { [name]: xs };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
withVal = (x) => ({ [name]: x });
|
|
||||||
}
|
|
||||||
|
|
||||||
const func = (...pats) => sequence(...pats).withValue(withVal);
|
|
||||||
|
|
||||||
const setter = function (...pats) {
|
|
||||||
if (!pats.length) {
|
|
||||||
return this.fmap(withVal);
|
|
||||||
}
|
|
||||||
return this.set(func(...pats));
|
|
||||||
};
|
|
||||||
Pattern.prototype[name] = setter;
|
|
||||||
return func;
|
|
||||||
};
|
|
||||||
|
|
||||||
generic_params.forEach(([names, ...aliases]) => {
|
|
||||||
const name = Array.isArray(names) ? names[0] : names;
|
|
||||||
controls[name] = controls.createParam(names);
|
|
||||||
|
|
||||||
aliases.forEach((alias) => {
|
|
||||||
controls[alias] = controls[name];
|
|
||||||
Pattern.prototype[alias] = Pattern.prototype[name];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
controls.createParams = (...names) =>
|
|
||||||
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
|
|
||||||
|
|
||||||
controls.adsr = register('adsr', (adsr, pat) => {
|
|
||||||
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
|
|
||||||
const [attack, decay, sustain, release] = adsr;
|
|
||||||
return pat.set({ attack, decay, sustain, release });
|
|
||||||
});
|
|
||||||
controls.ds = register('ds', (ds, pat) => {
|
|
||||||
ds = !Array.isArray(ds) ? [ds] : ds;
|
|
||||||
const [decay, sustain] = ds;
|
|
||||||
return pat.set({ decay, sustain });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default controls;
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
drawLine.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/drawLine.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 Fraction, { gcd } from './fraction.mjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intended for a debugging, drawLine renders the pattern as a string, where each character represents the same time span.
|
|
||||||
* Should only be used with single characters as values, otherwise the character slots will be messed up.
|
|
||||||
* Character legend:
|
|
||||||
*
|
|
||||||
* - "|" cycle separator
|
|
||||||
* - "-" hold previous value
|
|
||||||
* - "." silence
|
|
||||||
*
|
|
||||||
* @param {Pattern} pattern the pattern to use
|
|
||||||
* @param {number} chars max number of characters (approximately)
|
|
||||||
* @returns string
|
|
||||||
* @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;
|
|
||||||
let pos = Fraction(0);
|
|
||||||
let lines = [''];
|
|
||||||
let emptyLine = ''; // this will be the "reference" empty line, which will be copied into extra lines
|
|
||||||
while (lines[0].length < chars) {
|
|
||||||
const haps = pat.queryArc(cycle, cycle + 1);
|
|
||||||
const durations = haps.filter((hap) => hap.hasOnset()).map((hap) => hap.duration);
|
|
||||||
const charFraction = gcd(...durations);
|
|
||||||
const totalSlots = charFraction.inverse(); // number of character slots for the current cycle
|
|
||||||
lines = lines.map((line) => line + '|'); // add pipe character before each cycle
|
|
||||||
emptyLine += '|';
|
|
||||||
for (let i = 0; i < totalSlots; i++) {
|
|
||||||
const [begin, end] = [pos, pos.add(charFraction)];
|
|
||||||
const matches = haps.filter((hap) => hap.whole.begin.lte(begin) && hap.whole.end.gte(end));
|
|
||||||
const missingLines = matches.length - lines.length;
|
|
||||||
if (missingLines > 0) {
|
|
||||||
lines = lines.concat(Array(missingLines).fill(emptyLine));
|
|
||||||
}
|
|
||||||
lines = lines.map((line, i) => {
|
|
||||||
const hap = matches[i];
|
|
||||||
if (hap) {
|
|
||||||
const isOnset = hap.whole.begin.eq(begin);
|
|
||||||
const char = isOnset ? '' + hap.value : '-';
|
|
||||||
return line + char;
|
|
||||||
}
|
|
||||||
return line + '.';
|
|
||||||
});
|
|
||||||
emptyLine += '.';
|
|
||||||
pos = pos.add(charFraction);
|
|
||||||
}
|
|
||||||
cycle++;
|
|
||||||
}
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default drawLine;
|
|
||||||
@@ -1,171 +1,29 @@
|
|||||||
/*
|
import { Pattern, timeCat } from './strudel.mjs';
|
||||||
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
|
import bjork from 'bjork';
|
||||||
Copyright (C) 2023 Rohan Drape and strudel contributors
|
import { rotate } from './util.mjs';
|
||||||
|
|
||||||
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
|
|
||||||
|
|
||||||
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
|
|
||||||
https://rohandrape.net/?t=hmt
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
|
||||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
const left = function (n, x) {
|
const euclid = (pulses, steps, rotation = 0) => {
|
||||||
const [ons, offs] = n;
|
const b = bjork(steps, pulses);
|
||||||
const [xs, ys] = x;
|
|
||||||
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]));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* @memberof Pattern
|
|
||||||
* @name euclid
|
|
||||||
* @param {number} pulses the number of onsets / beats
|
|
||||||
* @param {number} steps the number of steps to fill
|
|
||||||
* @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)
|
|
||||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
|
||||||
* note("c3").euclid(3,7)
|
|
||||||
* @example // The Cuban tresillo pattern.
|
|
||||||
* note("c3").euclid(3,8)
|
|
||||||
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
|
|
||||||
* note("c3").euclid(4,7)
|
|
||||||
* @example // The Aksak rhythm of Turkey.
|
|
||||||
* note("c3").euclid(4,9)
|
|
||||||
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
|
|
||||||
* note("c3").euclid(4,11)
|
|
||||||
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
|
|
||||||
* note("c3").euclid(5,6)
|
|
||||||
* @example // The Nawakhat pattern, another popular Arab rhythm.
|
|
||||||
* note("c3").euclid(5,7)
|
|
||||||
* @example // The Cuban cinquillo pattern.
|
|
||||||
* note("c3").euclid(5,8)
|
|
||||||
* @example // A popular Arab rhythm called Agsag-Samai.
|
|
||||||
* note("c3").euclid(5,9)
|
|
||||||
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
|
|
||||||
* note("c3").euclid(5,11)
|
|
||||||
* @example // The Venda clapping pattern of a South African children’s song.
|
|
||||||
* note("c3").euclid(5,12)
|
|
||||||
* @example // The Bossa-Nova rhythm necklace of Brazil.
|
|
||||||
* note("c3").euclid(5,16)
|
|
||||||
* @example // A typical rhythm played on the Bendir (frame drum).
|
|
||||||
* note("c3").euclid(7,8)
|
|
||||||
* @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)
|
|
||||||
* @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)
|
|
||||||
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
|
||||||
* note("c3").euclidRot(13,24,5)
|
|
||||||
*/
|
|
||||||
|
|
||||||
const _euclidRot = function (pulses, steps, rotation) {
|
|
||||||
const b = bjork(pulses, steps);
|
|
||||||
if (rotation) {
|
if (rotation) {
|
||||||
return rotate(b, -rotation);
|
return rotate(b, -rotation);
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const euclid = register('euclid', function (pulses, steps, pat) {
|
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
||||||
return pat.struct(_euclidRot(pulses, steps, 0));
|
return this.struct(euclid(pulses, steps, rotation));
|
||||||
});
|
};
|
||||||
|
|
||||||
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
|
||||||
return pat.struct(_euclidRot(pulses, steps, rotation));
|
const bin_pat = euclid(pulses, steps, rotation);
|
||||||
});
|
const firstOne = bin_pat.indexOf(1);
|
||||||
|
const gapless = rotate(bin_pat, firstOne)
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
.join('')
|
.join('')
|
||||||
.split('1')
|
.split('1')
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((s) => [s.length + 1, true]);
|
.map((s) => [s.length + 1, true]);
|
||||||
return pat.struct(timeCat(...gapless));
|
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
|
export default euclid;
|
||||||
return _euclidLegato(pulses, steps, 0, pat);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
|
||||||
return _euclidLegato(pulses, steps, rotation, pat);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="seq('a', ['b', 'c'])"
|
value="cat('a', 'b')"
|
||||||
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
||||||
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
const getEvents = () => {
|
const getEvents = () => {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="seq('tomato', 'indigo', ['white', 'steelblue']).fast(4)"
|
value="cat('lime', slowcat('steelblue','darkblue'),'yellow','salmon','black').every(2,rev).fast(16)"
|
||||||
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
|
style="width: 100%; font-size: 1.5em; background: black; color: white; outline: none; position: absolute; top: 0"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
||||||
// this adds all strudel functions to the global scope, to be used by eval
|
// this adds all strudel functions to the global scope, to be used by eval
|
||||||
Object.assign(window, strudel);
|
Object.assign(window, strudel);
|
||||||
// setup elements
|
// setup elements
|
||||||
@@ -25,9 +25,10 @@
|
|||||||
function paint(code) {
|
function paint(code) {
|
||||||
const pattern = eval(code); // run code
|
const pattern = eval(code); // run code
|
||||||
const events = pattern.firstCycle(); // query first cycle
|
const events = pattern.firstCycle(); // query first cycle
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear canvas
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
ctx.fillStyle = event.value;
|
ctx.fillStyle = event.value;
|
||||||
ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration.valueOf() * canvas.width, canvas.height);
|
ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration * canvas.width, canvas.height);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<div style="position: absolute; top: 0; right: 0; padding: 4px">
|
||||||
|
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br />
|
||||||
|
<button id="stop" style="font-size: 2em">stop</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
style="font-size: 2em; background: #e8d565; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
|
||||||
|
id="text"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
Loading...</textarea
|
||||||
|
>
|
||||||
|
<script type="module">
|
||||||
|
document.body.style = 'margin: 0';
|
||||||
|
import * as strudel from 'https://cdn.skypack.dev/@strudel.cycles/core@0.0.2';
|
||||||
|
import 'https://cdn.skypack.dev/@strudel.cycles/core@0.0.2/euclid.mjs';
|
||||||
|
const { cat, State, TimeSpan } = strudel;
|
||||||
|
let pattern;
|
||||||
|
Object.assign(window, strudel); // add strudel to eval scope
|
||||||
|
const input = document.getElementById('text');
|
||||||
|
|
||||||
|
let initialCode = `sequence(880, [440, 660], 440, 660)
|
||||||
|
.div(slowcat(3,2).slow(2))
|
||||||
|
.off(1/8,mul(2))
|
||||||
|
.off(1/4,mul(3))
|
||||||
|
.legato(2)
|
||||||
|
.slow(2)`;
|
||||||
|
try {
|
||||||
|
initialCode = atob(decodeURIComponent(window.location.href.split('#')[1]));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('failed to decode', err);
|
||||||
|
}
|
||||||
|
input.value = initialCode;
|
||||||
|
|
||||||
|
const evaluate = () => {
|
||||||
|
try {
|
||||||
|
pattern = eval(input.value);
|
||||||
|
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.addEventListener('input', () => evaluate());
|
||||||
|
|
||||||
|
// helpers to create a worker dynamically without needing a server / extra file
|
||||||
|
const stringifyFunction = (func) => '(' + func + ')();';
|
||||||
|
const urlifyFunction = (func) =>
|
||||||
|
URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
|
||||||
|
const createWorker = (func) => new Worker(urlifyFunction(func));
|
||||||
|
|
||||||
|
const interval = 0.1;
|
||||||
|
const lookahead = 0.5;
|
||||||
|
|
||||||
|
// this class is basically the tale of two clocks
|
||||||
|
class Metro {
|
||||||
|
worker;
|
||||||
|
audioContext;
|
||||||
|
interval = 0.2; // query span
|
||||||
|
lastEnd = 0;
|
||||||
|
constructor(audioContext, callback, interval = this.interval) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
this.interval = interval;
|
||||||
|
this.worker = createWorker(() => {
|
||||||
|
// we cannot use closures here!
|
||||||
|
let interval;
|
||||||
|
let timerID = null; // this is clock #1 (the sloppy js clock)
|
||||||
|
const clear = () => {
|
||||||
|
if (timerID) {
|
||||||
|
clearInterval(timerID);
|
||||||
|
timerID = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const start = () => {
|
||||||
|
clear();
|
||||||
|
if (!interval) {
|
||||||
|
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
|
||||||
|
}
|
||||||
|
timerID = setInterval(() => postMessage('tick'), interval * 1000);
|
||||||
|
};
|
||||||
|
self.onmessage = function (e) {
|
||||||
|
if (e.data == 'start') {
|
||||||
|
start();
|
||||||
|
} else if (e.data.interval) {
|
||||||
|
interval = e.data.interval;
|
||||||
|
if (timerID) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
} else if (e.data == 'stop') {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.worker.postMessage({ interval });
|
||||||
|
const round = (n, d) => Math.round(n * d) / d;
|
||||||
|
const precision = 100;
|
||||||
|
this.worker.onmessage = (e) => {
|
||||||
|
if (e.data === 'tick') {
|
||||||
|
const begin = this.lastEnd || this.audioContext.currentTime;
|
||||||
|
const end = this.audioContext.currentTime + this.interval; // DONT reference begin here!
|
||||||
|
this.lastEnd = end;
|
||||||
|
// callback with query span, using clock #2 (the audio clock)
|
||||||
|
callback(begin, end);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
console.log('start...');
|
||||||
|
this.audioContext.resume();
|
||||||
|
this.worker.postMessage('start');
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
console.log('stop...');
|
||||||
|
this.worker.postMessage('stop');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioContext = new AudioContext();
|
||||||
|
const metro = new Metro(
|
||||||
|
audioContext,
|
||||||
|
(begin, end) => {
|
||||||
|
pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
|
||||||
|
if (!e.part.begin.equals(e.whole.begin)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.context.createAudioNode) {
|
||||||
|
e.context.createAudioNode(e);
|
||||||
|
} else {
|
||||||
|
// fallback sine wave
|
||||||
|
const attack = 0.01;
|
||||||
|
const decay = 0.05;
|
||||||
|
const sustain = 0.5;
|
||||||
|
const release = 0.01;
|
||||||
|
const velocity = (e.context?.velocity || 1) * 0.1;
|
||||||
|
const osc = audioContext.createOscillator();
|
||||||
|
osc.type = 'sine';
|
||||||
|
osc.frequency.value = e.value;
|
||||||
|
osc.start(e.whole.begin);
|
||||||
|
const envelope = adsr(attack, decay, sustain, release, velocity, e.whole.begin, e.whole.end);
|
||||||
|
osc.stop(e.whole.end + release);
|
||||||
|
osc.connect(envelope);
|
||||||
|
envelope.connect(audioContext.destination);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
interval,
|
||||||
|
lookahead,
|
||||||
|
);
|
||||||
|
|
||||||
|
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
gainNode.gain.setValueAtTime(0, begin);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||||
|
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
|
||||||
|
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
|
||||||
|
// for some reason, using exponential ramping creates little cracklings
|
||||||
|
return gainNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
strudel.Pattern.prototype.withAudioNode = function (createAudioNode) {
|
||||||
|
return this._withEvent((event) => {
|
||||||
|
return event.setContext({
|
||||||
|
...event.context,
|
||||||
|
createAudioNode: (e) => createAudioNode(e, event.context.createAudioNode?.(event)),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
strudel.Pattern.prototype._osc = function (type) {
|
||||||
|
return this.withAudioNode((e) => {
|
||||||
|
const osc = audioContext.createOscillator();
|
||||||
|
osc.type = type;
|
||||||
|
osc.frequency.value = e.value; // expects frequency..
|
||||||
|
osc.start(e.whole.begin.valueOf() + lookahead);
|
||||||
|
osc.stop(e.whole.end.valueOf() + lookahead); // release?
|
||||||
|
// osc.connect(audioContext.destination);
|
||||||
|
return osc;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
strudel.Pattern.prototype.define('osc', (type, pat) => pat.osc(type), { patternified: true });
|
||||||
|
strudel.Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
|
||||||
|
return this.withAudioNode((e, node) => {
|
||||||
|
const velocity = e.context?.velocity || 1;
|
||||||
|
const envelope = adsr(
|
||||||
|
a,
|
||||||
|
d,
|
||||||
|
s,
|
||||||
|
r,
|
||||||
|
velocity,
|
||||||
|
e.whole.begin.valueOf() + lookahead,
|
||||||
|
e.whole.end.valueOf() + lookahead,
|
||||||
|
);
|
||||||
|
node?.connect(envelope);
|
||||||
|
return envelope;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
strudel.Pattern.prototype.filter = function (type = 'lowshelf', frequency = 1000, gain = 25) {
|
||||||
|
return this.withAudioNode((e, node) => {
|
||||||
|
const filter = audioContext.createBiquadFilter();
|
||||||
|
filter.type = type;
|
||||||
|
filter.frequency.value = frequency;
|
||||||
|
filter.gain.value = gain;
|
||||||
|
node?.connect(filter);
|
||||||
|
return filter;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
strudel.Pattern.prototype.out = function () {
|
||||||
|
const master = audioContext.createGain();
|
||||||
|
master.gain.value = 0.1;
|
||||||
|
master.connect(audioContext.destination);
|
||||||
|
return this.withAudioNode((e, node) => {
|
||||||
|
if (!node) {
|
||||||
|
console.warn('out: no source! call .osc() first');
|
||||||
|
}
|
||||||
|
node?.connect(master);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('start').addEventListener('click', () => metro.start());
|
||||||
|
document.getElementById('stop').addEventListener('click', () => metro.stop());
|
||||||
|
evaluate(); // evaluate initial code
|
||||||
|
/*
|
||||||
|
sequence(1,3/2,1,2).stack(
|
||||||
|
stack(5,3,4).velocity(.8).slow(2).legato(.2)
|
||||||
|
//.echo(3, 1/4, .5)
|
||||||
|
).mul(110) // frequencies
|
||||||
|
.div(slowcat(slowcat(2,5/4, 3/2),slowcat(3,4/3)).slow(2))
|
||||||
|
.echoWith(
|
||||||
|
16, // n of partials / cutoff
|
||||||
|
tri2.slow(128).div(2), // time between partials
|
||||||
|
(x,n)=>x // n = partial index 0..n
|
||||||
|
.mul((n+1)*3/2)
|
||||||
|
.legato(2)
|
||||||
|
.velocity(1/((n+1)**1.6)) // amplitude falloff
|
||||||
|
).legato(2) // note length
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
@@ -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};
|
|
||||||
@@ -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)>")`;
|
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
/*
|
|
||||||
fraction.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/fraction.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 Fraction from 'fraction.js';
|
import Fraction from 'fraction.js';
|
||||||
import { TimeSpan } from './timespan.mjs';
|
import { TimeSpan } from './strudel.mjs';
|
||||||
|
|
||||||
// Returns the start of the cycle.
|
// Returns the start of the cycle.
|
||||||
Fraction.prototype.sam = function () {
|
Fraction.prototype.sam = function () {
|
||||||
@@ -22,11 +16,6 @@ Fraction.prototype.wholeCycle = function () {
|
|||||||
return new TimeSpan(this.sam(), this.nextSam());
|
return new TimeSpan(this.sam(), this.nextSam());
|
||||||
};
|
};
|
||||||
|
|
||||||
// The position of a time value relative to the start of its cycle.
|
|
||||||
Fraction.prototype.cyclePos = function () {
|
|
||||||
return this.sub(this.sam());
|
|
||||||
};
|
|
||||||
|
|
||||||
Fraction.prototype.lt = function (other) {
|
Fraction.prototype.lt = function (other) {
|
||||||
return this.compare(other) < 0;
|
return this.compare(other) < 0;
|
||||||
};
|
};
|
||||||
@@ -55,8 +44,7 @@ Fraction.prototype.min = function (other) {
|
|||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
Fraction.prototype.show = function () {
|
||||||
// return this.toFraction(excludeWhole);
|
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,17 +62,11 @@ const fraction = (n) => {
|
|||||||
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
||||||
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
||||||
*/
|
*/
|
||||||
// n = String(n); // this is actually faster but imprecise...
|
n = String(n);
|
||||||
}
|
}
|
||||||
return Fraction(n);
|
return Fraction(n);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gcd = (...fractions) => {
|
|
||||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
|
||||||
};
|
|
||||||
|
|
||||||
fraction._original = Fraction;
|
|
||||||
|
|
||||||
export default fraction;
|
export default fraction;
|
||||||
|
|
||||||
// "If you concern performance, cache Fraction.js objects and pass arrays/objects.“
|
// "If you concern performance, cache Fraction.js objects and pass arrays/objects.“
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
/*
|
|
||||||
gist.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/gist.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// this is a shortcut to eval code from a gist
|
// this is a shortcut to eval code from a gist
|
||||||
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
|
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
|
||||||
export default (route, cache = true) =>
|
export default (route) =>
|
||||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
|
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${Date.now()}`)
|
||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
.then((code) => eval(code));
|
.then((code) => eval(code));
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
/*
|
|
||||||
hap.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class Hap {
|
|
||||||
/*
|
|
||||||
Event class, representing a value active during the timespan
|
|
||||||
'part'. This might be a fragment of an event, in which case the
|
|
||||||
timespan will be smaller than the 'whole' timespan, otherwise the
|
|
||||||
two timespans will be the same. The 'part' must never extend outside of the
|
|
||||||
'whole'. If the event represents a continuously changing value
|
|
||||||
then the whole will be returned as None, in which case the given
|
|
||||||
value will have been sampled from the point halfway between the
|
|
||||||
start and end of the 'part' timespan.
|
|
||||||
The context is to store a list of source code locations causing the event.
|
|
||||||
|
|
||||||
The word 'Event' is more or less a reserved word in javascript, hence this
|
|
||||||
class is named called 'Hap'.
|
|
||||||
*/
|
|
||||||
|
|
||||||
constructor(whole, part, value, context = {}, stateful = false) {
|
|
||||||
this.whole = whole;
|
|
||||||
this.part = part;
|
|
||||||
this.value = value;
|
|
||||||
this.context = context;
|
|
||||||
this.stateful = stateful;
|
|
||||||
if (stateful) {
|
|
||||||
console.assert(typeof this.value === 'function', 'Stateful values must be functions');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get duration() {
|
|
||||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
get endClipped() {
|
|
||||||
return this.whole.begin.add(this.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
wholeOrPart() {
|
|
||||||
return this.whole ? this.whole : this.part;
|
|
||||||
}
|
|
||||||
|
|
||||||
withSpan(func) {
|
|
||||||
// Returns a new hap with the function f applies to the hap timespan.
|
|
||||||
const whole = this.whole ? func(this.whole) : undefined;
|
|
||||||
return new Hap(whole, func(this.part), this.value, this.context);
|
|
||||||
}
|
|
||||||
|
|
||||||
withValue(func) {
|
|
||||||
// Returns a new hap with the function f applies to the hap value.
|
|
||||||
return new Hap(this.whole, this.part, func(this.value), this.context);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasOnset() {
|
|
||||||
// Test whether the hap contains the onset, i.e that
|
|
||||||
// the beginning of the part is the same as that of the whole timespan."""
|
|
||||||
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveState(state) {
|
|
||||||
if (this.stateful && this.hasOnset()) {
|
|
||||||
console.log('stateful');
|
|
||||||
const func = this.value;
|
|
||||||
const [newState, newValue] = func(state);
|
|
||||||
return [newState, new Hap(this.whole, this.part, newValue, this.context, false)];
|
|
||||||
}
|
|
||||||
return [state, this];
|
|
||||||
}
|
|
||||||
|
|
||||||
spanEquals(other) {
|
|
||||||
return (this.whole == undefined && other.whole == undefined) || this.whole.equals(other.whole);
|
|
||||||
}
|
|
||||||
|
|
||||||
equals(other) {
|
|
||||||
return (
|
|
||||||
this.spanEquals(other) &&
|
|
||||||
this.part.equals(other.part) &&
|
|
||||||
// TODO would == be better ??
|
|
||||||
this.value === other.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
show(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 + ' ]';
|
|
||||||
}
|
|
||||||
|
|
||||||
showWhole(compact = false) {
|
|
||||||
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
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
combineContext(b) {
|
|
||||||
const a = this;
|
|
||||||
return { ...a.context, ...b.context, locations: (a.context.locations || []).concat(b.context.locations || []) };
|
|
||||||
}
|
|
||||||
|
|
||||||
setContext(context) {
|
|
||||||
return new Hap(this.whole, this.part, this.value, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
index.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/index.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';
|
|
||||||
export * from './euclid.mjs';
|
|
||||||
import Fraction from './fraction.mjs';
|
|
||||||
import { logger } from './logger.mjs';
|
|
||||||
export { Fraction, controls };
|
|
||||||
export * from './hap.mjs';
|
|
||||||
export * from './pattern.mjs';
|
|
||||||
export * from './signal.mjs';
|
|
||||||
export * from './state.mjs';
|
|
||||||
export * from './timespan.mjs';
|
|
||||||
export * from './util.mjs';
|
|
||||||
export * from './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 🌀');
|
|
||||||
if (globalThis._strudelLoaded) {
|
|
||||||
console.warn(
|
|
||||||
`@strudel.cycles/core was loaded more than once...
|
|
||||||
This might happen when you have multiple versions of strudel installed.
|
|
||||||
Please check with "npm ls @strudel.cycles/core".`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
globalThis._strudelLoaded = true;
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.8.2",
|
"version": "0.0.2",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "strudel.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "mocha --colors"
|
||||||
"build": "vite build",
|
|
||||||
"prepublishOnly": "pnpm build"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -25,17 +19,17 @@
|
|||||||
"algorave"
|
"algorave"
|
||||||
],
|
],
|
||||||
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
|
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fraction.js": "^4.2.0"
|
"bjork": "^0.0.1",
|
||||||
|
"fraction.js": "^4.2.0",
|
||||||
|
"ramda": "^0.28.0"
|
||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3",
|
"mocha": "^9.2.2"
|
||||||
"vitest": "^0.28.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
/*
|
|
||||||
signal.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.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 { Hap } from './hap.mjs';
|
|
||||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
|
||||||
import Fraction from './fraction.mjs';
|
|
||||||
import { id } from './util.mjs';
|
|
||||||
|
|
||||||
export function steady(value) {
|
|
||||||
// A continuous value
|
|
||||||
return new Pattern((state) => [new Hap(undefined, state.span, value)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const signal = (func) => {
|
|
||||||
const query = (state) => [new Hap(undefined, state.span, func(state.span.midpoint()))];
|
|
||||||
return new Pattern(query);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isaw = signal((t) => 1 - (t % 1));
|
|
||||||
export const isaw2 = isaw.toBipolar();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A sawtooth signal between 0 and 1.
|
|
||||||
*
|
|
||||||
* @return {Pattern}
|
|
||||||
* @example
|
|
||||||
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
|
||||||
* @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 sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A sine signal between 0 and 1.
|
|
||||||
*
|
|
||||||
* @return {Pattern}
|
|
||||||
* @example
|
|
||||||
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const sine = sine2.fromBipolar();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A cosine signal between 0 and 1.
|
|
||||||
*
|
|
||||||
* @return {Pattern}
|
|
||||||
* @example
|
|
||||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const cosine = sine._early(Fraction(1).div(4));
|
|
||||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A square signal between 0 and 1.
|
|
||||||
*
|
|
||||||
* @return {Pattern}
|
|
||||||
* @example
|
|
||||||
* square.segment(2).range(0,7).scale('C minor').note()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
|
||||||
export const square2 = square.toBipolar();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A triangle signal between 0 and 1.
|
|
||||||
*
|
|
||||||
* @return {Pattern}
|
|
||||||
* @example
|
|
||||||
* tri.segment(8).range(0,7).scale('C minor').note()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const tri = fastcat(isaw, saw);
|
|
||||||
export const tri2 = fastcat(isaw2, saw2);
|
|
||||||
|
|
||||||
export const time = signal(id);
|
|
||||||
|
|
||||||
// random signals
|
|
||||||
|
|
||||||
const xorwise = (x) => {
|
|
||||||
const a = (x << 13) ^ x;
|
|
||||||
const b = (a >> 17) ^ a;
|
|
||||||
return (b << 5) ^ b;
|
|
||||||
};
|
|
||||||
|
|
||||||
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
|
|
||||||
const _frac = (x) => x - Math.trunc(x);
|
|
||||||
|
|
||||||
const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
|
|
||||||
|
|
||||||
const intSeedToRand = (x) => (x % 536870912) / 536870912;
|
|
||||||
|
|
||||||
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
|
||||||
|
|
||||||
const timeToRandsPrime = (seed, n) => {
|
|
||||||
const result = [];
|
|
||||||
for (let i = 0; i < n; ++n) {
|
|
||||||
result.push(intSeedToRand(seed));
|
|
||||||
seed = xorwise(seed);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* @name rand
|
|
||||||
* @example
|
|
||||||
* // randomly change the cutoff
|
|
||||||
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const rand = signal(timeToRand);
|
|
||||||
/**
|
|
||||||
* A continuous pattern of random numbers, between -1 and 1
|
|
||||||
*/
|
|
||||||
export const rand2 = rand.toBipolar();
|
|
||||||
|
|
||||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
|
||||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
|
||||||
export const brand = _brandBy(0.5);
|
|
||||||
|
|
||||||
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A continuous pattern of random integers, between 0 and n-1.
|
|
||||||
*
|
|
||||||
* @name irand
|
|
||||||
* @param {number} n max value (exclusive)
|
|
||||||
* @example
|
|
||||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
|
||||||
* irand(8).struct("x(3,8)").scale('C minor').note()
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
|
||||||
|
|
||||||
export const __chooseWith = (pat, xs) => {
|
|
||||||
xs = xs.map(reify);
|
|
||||||
if (xs.length == 0) {
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Choose from the list of values (or patterns of values) using the given
|
|
||||||
* pattern of numbers, which should be in the range of 0..1
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const chooseWith = (pat, xs) => {
|
|
||||||
return __chooseWith(pat, xs).outerJoin();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As with {chooseWith}, but the structure comes from the chosen values, rather
|
|
||||||
* than the pattern you're using to choose with.
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @param {*} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
export const chooseInWith = (pat, xs) => {
|
|
||||||
return __chooseWith(pat, xs).innerJoin();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chooses randomly from the given list of elements.
|
|
||||||
* @param {...any} xs values / patterns to choose from.
|
|
||||||
* @returns {Pattern} - a continuous pattern.
|
|
||||||
*/
|
|
||||||
export const choose = (...xs) => chooseWith(rand, xs);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chooses from the given list of values (or patterns of values), according
|
|
||||||
* to the pattern that the method is called on. The pattern should be in
|
|
||||||
* the range 0 .. 1.
|
|
||||||
* @param {...any} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
Pattern.prototype.choose = function (...xs) {
|
|
||||||
return chooseWith(this, xs);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As with choose, but the pattern that this method is called on should be
|
|
||||||
* in the range -1 .. 1
|
|
||||||
* @param {...any} xs
|
|
||||||
* @returns {Pattern}
|
|
||||||
*/
|
|
||||||
Pattern.prototype.choose2 = function (...xs) {
|
|
||||||
return chooseWith(this.fromBipolar(), xs);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Picks one of the elements at random each cycle.
|
|
||||||
* @returns {Pattern}
|
|
||||||
* @example
|
|
||||||
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
|
||||||
* @example
|
|
||||||
* "bd | hh | sd".s().fast(4)
|
|
||||||
*/
|
|
||||||
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
|
||||||
|
|
||||||
export const randcat = chooseCycles;
|
|
||||||
|
|
||||||
const _wchooseWith = function (pat, ...pairs) {
|
|
||||||
const values = pairs.map((pair) => reify(pair[0]));
|
|
||||||
const weights = [];
|
|
||||||
let accum = 0;
|
|
||||||
for (const pair of pairs) {
|
|
||||||
accum += pair[1];
|
|
||||||
weights.push(accum);
|
|
||||||
}
|
|
||||||
const total = accum;
|
|
||||||
const match = function (r) {
|
|
||||||
const find = r * total;
|
|
||||||
return values[weights.findIndex((x) => x > find, weights)];
|
|
||||||
};
|
|
||||||
return pat.fmap(match);
|
|
||||||
};
|
|
||||||
|
|
||||||
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
|
||||||
|
|
||||||
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);
|
|
||||||
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
|
|
||||||
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
|
|
||||||
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
|
||||||
*
|
|
||||||
* @name perlin
|
|
||||||
* @example
|
|
||||||
* // randomly change the cutoff
|
|
||||||
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
|
||||||
|
|
||||||
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
|
||||||
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Randomly removes events from the pattern by a given amount.
|
|
||||||
* 0 = 0% chance of removal
|
|
||||||
* 1 = 100% chance of removal
|
|
||||||
*
|
|
||||||
* @name degradeBy
|
|
||||||
* @memberof Pattern
|
|
||||||
* @param {number} amount - a number between 0 and 1
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").degradeBy(0.2)
|
|
||||||
* @example
|
|
||||||
* s("[hh?0.2]*8")
|
|
||||||
*/
|
|
||||||
export const degradeBy = register('degradeBy', function (x, pat) {
|
|
||||||
return pat._degradeByWith(rand, x);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
|
|
||||||
*
|
|
||||||
* @name degrade
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").degrade()
|
|
||||||
* @example
|
|
||||||
* s("[hh?]*8")
|
|
||||||
*/
|
|
||||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
|
||||||
* 0 = 100% chance of removal
|
|
||||||
* 1 = 0% chance of removal
|
|
||||||
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
|
||||||
*
|
|
||||||
* @name undegradeBy
|
|
||||||
* @memberof Pattern
|
|
||||||
* @param {number} amount - a number between 0 and 1
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").undegradeBy(0.2)
|
|
||||||
*/
|
|
||||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
|
||||||
return pat._degradeByWith(
|
|
||||||
rand.fmap((r) => 1 - r),
|
|
||||||
x,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Randomly applies the given function by the given probability.
|
|
||||||
* Similar to {@link Pattern#someCyclesBy}
|
|
||||||
*
|
|
||||||
* @name sometimesBy
|
|
||||||
* @memberof Pattern
|
|
||||||
* @param {number | Pattern} probability - a number between 0 and 1
|
|
||||||
* @param {function} function - the transformation to apply
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
|
||||||
return reify(patx)
|
|
||||||
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
|
||||||
.innerJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Applies the given function with a 50% chance
|
|
||||||
*
|
|
||||||
* @name sometimes
|
|
||||||
* @memberof Pattern
|
|
||||||
* @param {function} function - the transformation to apply
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
export const sometimes = register('sometimes', function (func, pat) {
|
|
||||||
return pat._sometimesBy(0.5, func);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
|
||||||
* Similar to {@link Pattern#sometimesBy}
|
|
||||||
*
|
|
||||||
* @name someCyclesBy
|
|
||||||
* @memberof Pattern
|
|
||||||
* @param {number | Pattern} probability - a number between 0 and 1
|
|
||||||
* @param {function} function - the transformation to apply
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
|
||||||
return reify(patx)
|
|
||||||
.fmap((x) =>
|
|
||||||
stack(
|
|
||||||
pat._degradeByWith(rand._segment(1), x),
|
|
||||||
func(pat._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.innerJoin();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Shorthand for `.someCyclesBy(0.5, fn)`
|
|
||||||
*
|
|
||||||
* @name someCycles
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @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);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Shorthand for `.sometimesBy(0.75, fn)`
|
|
||||||
*
|
|
||||||
* @name often
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").often(x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
export const often = register('often', function (func, pat) {
|
|
||||||
return pat.sometimesBy(0.75, func);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Shorthand for `.sometimesBy(0.25, fn)`
|
|
||||||
*
|
|
||||||
* @name rarely
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
export const rarely = register('rarely', function (func, pat) {
|
|
||||||
return pat.sometimesBy(0.25, func);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Shorthand for `.sometimesBy(0.1, fn)`
|
|
||||||
*
|
|
||||||
* @name almostNever
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
export const almostNever = register('almostNever', function (func, pat) {
|
|
||||||
return pat.sometimesBy(0.1, func);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Shorthand for `.sometimesBy(0.9, fn)`
|
|
||||||
*
|
|
||||||
* @name almostAlways
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
export const almostAlways = register('almostAlways', function (func, pat) {
|
|
||||||
return pat.sometimesBy(0.9, func);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
|
||||||
*
|
|
||||||
* @name never
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").never(x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
export const never = register('never', function (_, pat) {
|
|
||||||
return pat;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
|
||||||
*
|
|
||||||
* @name always
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("hh*8").always(x=>x.speed("0.5"))
|
|
||||||
*/
|
|
||||||
export const always = register('always', function (func, pat) {
|
|
||||||
return func(pat);
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
speak.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/speak.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 { register } from './index.mjs';
|
|
||||||
|
|
||||||
let synth;
|
|
||||||
try {
|
|
||||||
synth = window?.speechSynthesis;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('cannot use window: not in browser?');
|
|
||||||
}
|
|
||||||
|
|
||||||
let allVoices = synth?.getVoices();
|
|
||||||
// console.log('voices', allVoices);
|
|
||||||
|
|
||||||
function triggerSpeech(words, lang, voice) {
|
|
||||||
synth.cancel();
|
|
||||||
const utterance = new SpeechSynthesisUtterance(words);
|
|
||||||
utterance.lang = lang;
|
|
||||||
allVoices = synth.getVoices();
|
|
||||||
const voices = allVoices.filter((v) => v.lang.includes(lang));
|
|
||||||
if (typeof voice === 'number') {
|
|
||||||
utterance.voice = voices[voice % voices.length];
|
|
||||||
} else if (typeof voice === 'string') {
|
|
||||||
utterance.voice = voices.find((voice) => voice.name === voice);
|
|
||||||
}
|
|
||||||
// console.log(utterance.voice?.name, utterance.voice?.lang);
|
|
||||||
speechSynthesis.speak(utterance);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const speak = register('speak', function (lang, voice, pat) {
|
|
||||||
return pat.onTrigger((_, hap) => {
|
|
||||||
triggerSpeech(hap.value, lang, voice);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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 @@
|
|||||||
/*
|
|
||||||
state.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/state.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class State {
|
|
||||||
constructor(span, controls = {}) {
|
|
||||||
this.span = span;
|
|
||||||
this.controls = controls;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns new State with different span
|
|
||||||
setSpan(span) {
|
|
||||||
return new State(span, this.controls);
|
|
||||||
}
|
|
||||||
|
|
||||||
withSpan(func) {
|
|
||||||
return this.setSpan(func(this.span));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns new State with different controls
|
|
||||||
setControls(controls) {
|
|
||||||
return new State(this.span, controls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default State;
|
|
||||||
@@ -1,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 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
drawLine.test.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/drawLine.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 { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs';
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import drawLine from '../drawLine.mjs';
|
|
||||||
|
|
||||||
describe('drawLine', () => {
|
|
||||||
it('supports equal lengths', () => {
|
|
||||||
expect(drawLine(fastcat(0), 4)).toEqual('|0|0');
|
|
||||||
expect(drawLine(fastcat(0, 1), 4)).toEqual('|01|01');
|
|
||||||
expect(drawLine(fastcat(0, 1, 2), 6)).toEqual('|012|012');
|
|
||||||
});
|
|
||||||
it('supports unequal lengths', () => {
|
|
||||||
expect(drawLine(fastcat(0, [1, 2]), 10)).toEqual('|0-12|0-12');
|
|
||||||
expect(drawLine(fastcat(0, [1, 2, 3]), 10)).toEqual('|0--123|0--123');
|
|
||||||
expect(drawLine(fastcat(0, 1, [2, 3]), 10)).toEqual('|0-1-23|0-1-23');
|
|
||||||
});
|
|
||||||
it('supports unequal silence', () => {
|
|
||||||
expect(drawLine(fastcat(0, silence, [1, 2]), 10)).toEqual('|0-..12|0-..12');
|
|
||||||
});
|
|
||||||
it('supports polyrhythms', () => {
|
|
||||||
// assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
|
|
||||||
expect(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10)).toEqual('|0--0--1-1-1-');
|
|
||||||
});
|
|
||||||
it('supports multiple lines', () => {
|
|
||||||
expect(drawLine(fastcat(0, stack(1, 2)), 10)).toEqual(`|01|01|01|01
|
|
||||||
|.2|.2|.2|.2`);
|
|
||||||
|
|
||||||
expect(drawLine(fastcat(0, 1, stack(2, 3)), 10)).toEqual(`|012|012|012
|
|
||||||
|..3|..3|..3`);
|
|
||||||
|
|
||||||
expect(drawLine(fastcat(0, stack(1, 2, 3)), 10)).toEqual(`|01|01|01|01
|
|
||||||
|.2|.2|.2|.2
|
|
||||||
|.3|.3|.3|.3`);
|
|
||||||
expect(drawLine(fastcat(0, 1, stack(2, 3, 4)), 10)).toEqual(`|012|012|012
|
|
||||||
|..3|..3|..3
|
|
||||||
|..4|..4|..4`);
|
|
||||||
});
|
|
||||||
it('supports unequal cycle lengths', () => {
|
|
||||||
expect(drawLine(slowcat(0, [1, 2]), 10)).toEqual(`|0|12|0|12`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
fraction.test.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/fraction.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 Fraction, { gcd } from '../fraction.mjs';
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('gcd', () => {
|
|
||||||
it('should work', () => {
|
|
||||||
const F = Fraction._original;
|
|
||||||
expect(gcd(F(1 / 6), F(1 / 4)).toFraction()).toEqual('1/12');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||