mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 14:53:45 -04:00
Compare commits
309 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f17a4d045f | |||
| 8e1e83f3cf | |||
| 2d64b538e7 | |||
| b717615558 | |||
| 8c4f10464b | |||
| 018649c4f3 | |||
| 5ff59a878d | |||
| a20468b85f | |||
| 9648e35de2 | |||
| cd344cefe0 | |||
| 1f6a313f02 | |||
| 5cbeb227d8 | |||
| 2ab2cc5fc8 | |||
| 3dd667e825 | |||
| 482ebb695a | |||
| 218952b8b6 | |||
| 2e8a8b50df | |||
| f22f89347e | |||
| de15d79edf | |||
| 23e5358ae4 | |||
| 3b6777e259 | |||
| ed5bf3b3fc | |||
| b089328da3 | |||
| 784c711870 | |||
| 34b10b6c84 | |||
| 450f9685f3 | |||
| 03090e399c | |||
| ed58d26da6 | |||
| e3605590e5 | |||
| d55ef8e843 | |||
| efc2333360 | |||
| 92626e7c20 | |||
| ad113b3888 | |||
| 39c45bc2b2 | |||
| 899af88bd9 | |||
| c2b8cc4b82 | |||
| 40ed7b5382 | |||
| 5d829419ba | |||
| e26da854c0 | |||
| a28b7a2d5a | |||
| 83c776d70f | |||
| 051efdc13b | |||
| ef3cd56b0d | |||
| 9017085fa3 | |||
| cdbbe079db | |||
| d6045c943b | |||
| 6ee9dea2ff | |||
| 03cbe4b6d4 | |||
| 784ee576da | |||
| ee3e1217b4 | |||
| 30c80119e9 | |||
| 973ae4c7da | |||
| 4d13b981f1 | |||
| 7691403fd0 | |||
| 3b6e3624be | |||
| 51f0bab1f8 | |||
| dae1560c40 | |||
| 82f0dbb750 | |||
| fb7d76a2ab | |||
| 26225e4ea7 | |||
| ab12c6ae0a | |||
| 4a894b0be2 | |||
| 4ad305550e | |||
| cf5c502a05 | |||
| 2415210be6 | |||
| fe18aa770d | |||
| e136e95375 | |||
| 004c0baa59 | |||
| 5f44ae9c4b | |||
| 66828e17d3 | |||
| b2f90ecb3b | |||
| ccf2a6de52 | |||
| 9416f317d4 | |||
| 75b6f59212 | |||
| 84238c8999 | |||
| 2d855cf5e8 | |||
| dd04d49c75 | |||
| 34874862ab | |||
| 0361bc629f | |||
| 1fdd87a4f3 | |||
| 16e9aca222 | |||
| 4a3d5ca44f | |||
| 6c0e8a0833 | |||
| 7336ff0ded | |||
| 735f57e276 | |||
| eb4f1f69f6 | |||
| e7e321fe03 | |||
| bf3de368f0 | |||
| d645cc6fe5 | |||
| 6fe11be218 | |||
| cf7fc23981 | |||
| bfdfab0a00 | |||
| edb347e85a | |||
| 63fcce79e0 | |||
| 8defdc424c | |||
| 16b2fc0632 | |||
| d0a544d7a7 | |||
| 9b3c6b9f94 | |||
| 6327e17feb | |||
| 15fe2734e9 | |||
| 1dd1ce254f | |||
| 3ea91adfd6 | |||
| ce5f425142 | |||
| 671a22fc22 | |||
| 976955dae3 | |||
| a6ad58a375 | |||
| 681eed3d0e | |||
| 81a9811803 | |||
| 72b582c605 | |||
| 2f0891d4e5 | |||
| a7a283a99e | |||
| d9e44dcda6 | |||
| acd5661106 | |||
| b0b2b6a120 | |||
| 664d76cf1b | |||
| a901e2e387 | |||
| 82b14b5292 | |||
| 05bcd5e32f | |||
| 077556e7cb | |||
| 4cc2de9b7e | |||
| b0c0a393a1 | |||
| 168269a839 | |||
| 34771f03d4 | |||
| 504fe9ed40 | |||
| caf6686d01 | |||
| c063fb6b1c | |||
| e3680b96de | |||
| 688e275e0c | |||
| 2e33569786 | |||
| 46fe6d3c4d | |||
| 9220b95677 | |||
| 5b27b19972 | |||
| 5a72ea94b1 | |||
| eac6f27ca4 | |||
| f2330a2307 | |||
| db413881db | |||
| 77bab433e0 | |||
| f8cc1c2022 | |||
| e0bf9ea4d6 | |||
| 374661eb23 | |||
| df06248c54 | |||
| 27073d6c17 | |||
| f44caf9096 | |||
| ff5b11f5ed | |||
| 719c70a598 | |||
| 7a53f6c021 | |||
| 09d05abd70 | |||
| 3acc364a03 | |||
| 2dd528134a | |||
| fef50e654c | |||
| b1aba294ad | |||
| 63ac80eb79 | |||
| 82e3f9b837 | |||
| e19fd24447 | |||
| fe9a91d1e4 | |||
| 860bb58d61 | |||
| d58b3e490b | |||
| 0003ce174e | |||
| 8e0f23e081 | |||
| 71e60ed8de | |||
| e2a29914c7 | |||
| a9a3d491a2 | |||
| a750764f88 | |||
| 5b397009ab | |||
| e68df9e789 | |||
| 4d3bbd5057 | |||
| 15f3321b04 | |||
| 67bfedb317 | |||
| c0184dd827 | |||
| 09fdc45ff2 | |||
| c01b4c651c | |||
| 634eed1d82 | |||
| 01810f379f | |||
| f3c4afaa54 | |||
| a8757fecc8 | |||
| a1d181d609 | |||
| 440f1cb081 | |||
| 95193b0bfd | |||
| 66673d211e | |||
| 64ab3c9ba7 | |||
| 6c4aa12c60 | |||
| a8f2537e31 | |||
| 7e6876c282 | |||
| 1cc15c7e8d | |||
| 10ac7c353c | |||
| 9412bf60bf | |||
| c751bb7e5f | |||
| 28cd7c1681 | |||
| 011139eff8 | |||
| 184cb746f3 | |||
| 1b7fbecf50 | |||
| 117cf7e18a | |||
| ae15bb7274 | |||
| 1b3b07842e | |||
| 98efd72ab4 | |||
| 1eb5f6a995 | |||
| 9b10dc8535 | |||
| 2951a60fac | |||
| 94257e81ee | |||
| 006ce9d1da | |||
| 607a6121bc | |||
| 4208c79c09 | |||
| 7565354274 | |||
| ffe831d24b | |||
| 60dd8bd763 | |||
| 29e5833c8c | |||
| e3a6444f41 | |||
| 30fd4c5dec | |||
| 2da03ab46f | |||
| 243adda3dd | |||
| 7fc21d55d7 | |||
| d5ef686fe0 | |||
| d4733a30ab | |||
| 989bd0990e | |||
| 60d09adb44 | |||
| b2588e8c93 | |||
| 39d2784441 | |||
| aa70246afc | |||
| f6ffdd6ae6 | |||
| 077aac1888 | |||
| 94a3a60e49 | |||
| f2372e7a41 | |||
| a0fb8fb37f | |||
| b4027fd92a | |||
| c9cafa37fd | |||
| 2e017e46f9 | |||
| c9f58220a3 | |||
| e9de45993e | |||
| 6b2b8b5124 | |||
| 64a6dacc1e | |||
| 1738e4d538 | |||
| abc7a1e48d | |||
| 7716fdb98e | |||
| 2938bfd88f | |||
| 222e479e30 | |||
| 92b5b6538f | |||
| 3a19e23473 | |||
| dd5743ab8c | |||
| 51e3aa257c | |||
| 5eabcf0618 | |||
| 6d21456423 | |||
| bca16cdf99 | |||
| 492271d786 | |||
| f71db4aeed | |||
| 5d4ef46ac7 | |||
| bd93e44154 | |||
| 6fd89ddee7 | |||
| cf1f4ec35c | |||
| 12bb82d295 | |||
| c33cfc78c5 | |||
| ac6472a43a | |||
| 989fdfa20b | |||
| 05ae31838e | |||
| c691916cbf | |||
| e16295623d | |||
| 9626f3cc9a | |||
| 432e8dcccc | |||
| d2e93a99f1 | |||
| 3505732afa | |||
| 55a5f1d62a | |||
| 3657e2fd65 | |||
| 65a7b3030e | |||
| 208706fb52 | |||
| e04f250adb | |||
| 642ddcdb59 | |||
| 88ca54461e | |||
| 129fd7d6b0 | |||
| 7db52b3dc2 | |||
| 40f6489111 | |||
| 118b619b74 | |||
| 2dd445c102 | |||
| 3c3832dbce | |||
| 9b8761bc45 | |||
| 77ade0758e | |||
| 902012759a | |||
| ea61627963 | |||
| 95526ac99c | |||
| b0da353115 | |||
| fae59a6bcd | |||
| 11196fb1e6 | |||
| e782dc09dd | |||
| e3df504423 | |||
| b71b1354c3 | |||
| dbe3915368 | |||
| 62705da3de | |||
| 81ca199a96 | |||
| bee8c81254 | |||
| 8ad48e1633 | |||
| 4a181d5b33 | |||
| d5ab6b7211 | |||
| 4ba9d47bff | |||
| c6c5a3035c | |||
| e024ae2142 | |||
| 19df19375d | |||
| cba96049a3 | |||
| 1509ff1793 | |||
| 6a33032da7 | |||
| 6c05a4eb7b | |||
| 8d8b8ab99e | |||
| a4f10d5339 | |||
| 16472b59dd | |||
| 33a73c37bc | |||
| 4e6d39666a | |||
| a5b6022506 | |||
| 7df663e87d | |||
| 3159f66560 | |||
| 0d900c0134 | |||
| 66a27a609c | |||
| 496d60dc1c |
@@ -1,6 +1,6 @@
|
||||
name: Strudel tests
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -11,13 +11,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: apt install ztd
|
||||
run: apt update && apt install -y zstd
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
# cache: 'pnpm'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm run format-check
|
||||
- run: pnpm run lint
|
||||
|
||||
+77
-1
@@ -17,7 +17,7 @@ git remote set-url origin git@codeberg.org:uzu/strudel.git
|
||||
|
||||
To get in touch with the contributors, either
|
||||
|
||||
- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel
|
||||
- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel
|
||||
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
|
||||
|
||||
## Ask a Question
|
||||
@@ -150,6 +150,7 @@ Important: Always publish with `pnpm`, as `npm` does not support overriding main
|
||||
|
||||
|
||||
## useful commands
|
||||
|
||||
```sh
|
||||
#regenerate the test snapshots (ex: when updating or creating new pattern functions)
|
||||
pnpm snapshot
|
||||
@@ -160,6 +161,81 @@ pnpm run osc
|
||||
#build the standalone version
|
||||
pnpm tauri build
|
||||
```
|
||||
|
||||
## version tag patching
|
||||
|
||||
here's a little guide on how to patch patterns in the database to prevent breaking old patterns due to breaking changes in newer versions.
|
||||
|
||||
the general tactic is to use `// @version x.y` to tag a pattern with a specific strudel version. when a pattern is evaluated, this metadata will de-activate any breaking changes that came after the specified version.
|
||||
for example, in version 1.1, the default value for `fanchor` was changed from `0.5` to `0`.
|
||||
if play a pattern that was made before that change, sounds that use filter evenlopes can sound very different, so by adding `// @version 1.0` will make it sound like it used to.
|
||||
before releasing a new version with breaking changes, we can edit all patterns in the database, inserting the version tag they were created under:
|
||||
|
||||
as an example, to release version 1.2, do the following:
|
||||
|
||||
1. get date range
|
||||
|
||||
```sh
|
||||
# get date of last version:
|
||||
git log -1 --format=%aI @strudel/core@1.1.0
|
||||
# 2024-05-31T23:07:26+02:00
|
||||
|
||||
# get date of current version:
|
||||
git log -1 --format=%aI @strudel/core@1.2.0
|
||||
# 2025-05-01T12:39:24+02:00
|
||||
# might also use todays timestamp if version is not yet released
|
||||
```
|
||||
|
||||
now we know, all patterns between these 2 dates have to receive a version tag (unless they already have one).
|
||||
|
||||
2. get patterns in question
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM code_v1
|
||||
WHERE code NOT LIKE '%@version%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
this gives us all unversioned patterns that were saved between 1.1.0 and 1.2.0. in this case, it's 9373 patterns!
|
||||
|
||||
3. insert version tags
|
||||
|
||||
we are now ready to insert the version tag to these patterns.
|
||||
before updating thousands of patterns, it's probably a good idea to test if a single one gets udpated:
|
||||
|
||||
```sql
|
||||
UPDATE code_v1
|
||||
SET code = code || E'\n// @version 1.1'
|
||||
WHERE hash = 'Ns2sMB40yIw4';
|
||||
```
|
||||
|
||||
after [verifying](https://strudel.cc/?Ns2sMB40yIw4) that the version tag has been added, let's insert it everywhere:
|
||||
|
||||
```sql
|
||||
UPDATE code_v1
|
||||
SET code = code || E'\n// @version 1.1'
|
||||
WHERE code NOT LIKE '%@version%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
```
|
||||
|
||||
4. verify
|
||||
|
||||
we can verify that the edits worked by querying all patterns that contain the new version tag:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM code_v1
|
||||
WHERE code LIKE '%@version 1.1%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
|
||||
## Have Fun
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
@@ -32,19 +32,13 @@ This project is organized into many [packages](./packages), which are also avail
|
||||
|
||||
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
||||
|
||||
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||
|
||||
Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository.
|
||||
|
||||
## Contributing
|
||||
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||
|
||||
<a href="https://codeberg.org/uzu/strudel/activity/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors).
|
||||
|
||||
## Community
|
||||
|
||||
|
||||
@@ -83,4 +83,14 @@ export default [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Properties provided by AudioWorkletGlobalScope
|
||||
files: ['packages/superdough/worklets.mjs'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
currentTime: 'readonly',
|
||||
sampleRate: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
jsdoc-synonyms.js - Add support for @synonym tag
|
||||
jsdoc-synonyms.js - Add support for @synonyms tag
|
||||
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { history, indentWithTab } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
@@ -37,6 +37,14 @@ const extensions = {
|
||||
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
|
||||
isFlashEnabled,
|
||||
keybindings,
|
||||
isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []),
|
||||
isMultiCursorEnabled: (on) =>
|
||||
on
|
||||
? [
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||
]
|
||||
: [],
|
||||
};
|
||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
|
||||
@@ -51,6 +59,8 @@ export const defaultSettings = {
|
||||
isFlashEnabled: true,
|
||||
isTooltipEnabled: false,
|
||||
isLineWrappingEnabled: false,
|
||||
isTabIndentationEnabled: false,
|
||||
isMultiCursorEnabled: false,
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
@@ -62,7 +72,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
||||
});
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
|
||||
const settings = codemirrorSettings.get();
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
@@ -75,7 +85,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
/* search(),
|
||||
highlightSelectionMatches(), */
|
||||
...initialSettings,
|
||||
javascript(),
|
||||
mondo ? [] : javascript(),
|
||||
sliderPlugin,
|
||||
widgetPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
@@ -209,6 +219,7 @@ export class StrudelMirror {
|
||||
},
|
||||
onEvaluate: () => this.evaluate(),
|
||||
onStop: () => this.stop(),
|
||||
mondo: replOptions.mondo,
|
||||
});
|
||||
const cmEditor = this.root.querySelector('.cm-editor');
|
||||
if (cmEditor) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
||||
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setMiniLocations = StateEffect.define();
|
||||
@@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => {
|
||||
setTimeout(() => {
|
||||
updateMiniLocations(config.editor, config.miniLocations);
|
||||
}, 100);
|
||||
return on ? highlightExtension : [];
|
||||
return on ? Prec.highest(highlightExtension) : [];
|
||||
};
|
||||
|
||||
@@ -21,11 +21,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||
const keymaps = {
|
||||
vim,
|
||||
emacs,
|
||||
codemirror: () => keymap.of(defaultKeymap),
|
||||
vscode: vscodeExtension,
|
||||
};
|
||||
|
||||
export function keybindings(name) {
|
||||
const active = keymaps[name];
|
||||
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
|
||||
// keymap.of(searchKeymap),
|
||||
return [active ? active() : [], keymap.of(historyKeymap)];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
Vendored
+12
@@ -8,6 +8,11 @@ import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
|
||||
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
|
||||
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
|
||||
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
|
||||
import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs';
|
||||
import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs';
|
||||
|
||||
import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs';
|
||||
|
||||
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
||||
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
|
||||
import aura, { settings as auraSettings } from './themes/aura.mjs';
|
||||
@@ -39,17 +44,20 @@ import { setTheme } from '@strudel/draw';
|
||||
export const themes = {
|
||||
strudelTheme,
|
||||
algoboy,
|
||||
archBtw,
|
||||
androidstudio,
|
||||
atomone,
|
||||
aura,
|
||||
bbedit,
|
||||
blackscreen,
|
||||
bluescreen,
|
||||
bluescreenlight,
|
||||
CutiePi,
|
||||
darcula,
|
||||
dracula,
|
||||
duotoneDark,
|
||||
eclipse,
|
||||
fruitDaw,
|
||||
githubDark,
|
||||
githubLight,
|
||||
greenText,
|
||||
@@ -78,10 +86,12 @@ export const themes = {
|
||||
export const settings = {
|
||||
strudelTheme: strudelThemeSettings,
|
||||
bluescreen: bluescreenSettings,
|
||||
bluescreenlight: bluescreenlightsettings,
|
||||
blackscreen: blackscreenSettings,
|
||||
whitescreen: whitescreenSettings,
|
||||
teletext: teletextSettings,
|
||||
algoboy: algoboySettings,
|
||||
archBtw: archBtwSettings,
|
||||
androidstudio: androidstudioSettings,
|
||||
atomone: atomOneSettings,
|
||||
aura: auraSettings,
|
||||
@@ -92,9 +102,11 @@ export const settings = {
|
||||
eclipse: eclipseSettings,
|
||||
CutiePi: CutiePiSettings,
|
||||
sonicPink: sonicPinkSettings,
|
||||
fruitDaw: fruitDawSettings,
|
||||
githubLight: githubLightSettings,
|
||||
githubDark: githubDarkSettings,
|
||||
greenText: greenTextSettings,
|
||||
|
||||
gruvboxDark: gruvboxDarkSettings,
|
||||
gruvboxLight: gruvboxLightSettings,
|
||||
materialDark: materialDarkSettings,
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Arch Btw
|
||||
* Modern terminal inspired theme
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[1],
|
||||
selection: hex[2],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[0],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[1],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[1] },
|
||||
{ tag: t.keyword, color: hex[1] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
|
||||
],
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* A lighter blue screen theme
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[2],
|
||||
selection: hex[3],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[1],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[2],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] },
|
||||
{ tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[2] },
|
||||
{ tag: t.keyword, color: hex[2] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
|
||||
],
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Fruit Daw
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = [
|
||||
'rgb(84, 93, 98)',
|
||||
'rgb(255, 255, 255)',
|
||||
'rgba(255, 255, 255, .25)',
|
||||
'rgb(67, 76, 81)',
|
||||
'rgb(186, 230, 115)',
|
||||
'rgb(252, 184, 67)',
|
||||
'rgb(124, 206, 254)',
|
||||
'rgb(83, 101, 102)',
|
||||
'rgba(46, 62, 72,.5)',
|
||||
'rgb(94, 100, 108)',
|
||||
'rgb(167, 216, 177)',
|
||||
];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[10],
|
||||
selection: hex[8],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[3],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[3],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[1],
|
||||
},
|
||||
{ tag: [t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
|
||||
{ tag: [t.variableName], color: hex[1] },
|
||||
{ tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[6] },
|
||||
{ tag: t.keyword, color: hex[5] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] },
|
||||
],
|
||||
});
|
||||
+158
-22
@@ -4,13 +4,14 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
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 { Pattern, register, reify } from './pattern.mjs';
|
||||
|
||||
export function createParam(names) {
|
||||
let isMulti = Array.isArray(names);
|
||||
names = !isMulti ? [names] : names;
|
||||
const name = names[0];
|
||||
|
||||
// todo: make this less confusing
|
||||
const withVal = (xs) => {
|
||||
let bag;
|
||||
// check if we have an object with an unnamed control (.value)
|
||||
@@ -35,25 +36,34 @@ export function createParam(names) {
|
||||
}
|
||||
};
|
||||
|
||||
const func = (...pats) => sequence(...pats).withValue(withVal);
|
||||
|
||||
const setter = function (...pats) {
|
||||
if (!pats.length) {
|
||||
return this.fmap(withVal);
|
||||
// todo: make this less confusing
|
||||
const func = function (value, pat) {
|
||||
if (!pat) {
|
||||
return reify(value).withValue(withVal);
|
||||
}
|
||||
return this.set(func(...pats));
|
||||
if (typeof value === 'undefined') {
|
||||
return pat.fmap(withVal);
|
||||
}
|
||||
return pat.set(reify(value).withValue(withVal));
|
||||
};
|
||||
Pattern.prototype[name] = function (value) {
|
||||
return func(value, this);
|
||||
};
|
||||
Pattern.prototype[name] = setter;
|
||||
return func;
|
||||
}
|
||||
|
||||
// maps control alias names to the "main" control name
|
||||
const controlAlias = new Map();
|
||||
|
||||
export function isControlName(name) {
|
||||
return controlAlias.has(name);
|
||||
}
|
||||
|
||||
export function registerControl(names, ...aliases) {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
let bag = {};
|
||||
bag[name] = createParam(names);
|
||||
controlAlias.set(name, name);
|
||||
aliases.forEach((alias) => {
|
||||
bag[alias] = bag[name];
|
||||
controlAlias.set(alias, name);
|
||||
@@ -242,6 +252,20 @@ export const { fmenv } = registerControl('fmenv');
|
||||
*
|
||||
*/
|
||||
export const { fmattack } = registerControl('fmattack');
|
||||
|
||||
/**
|
||||
* waveform of the fm modulator
|
||||
*
|
||||
* @name fmwave
|
||||
* @param {number | Pattern} wave waveform
|
||||
* @example
|
||||
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("<sine square sawtooth crackle>").fm(4).fmh(2.01)
|
||||
* @example
|
||||
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
||||
*
|
||||
*/
|
||||
export const { fmwave } = registerControl('fmwave');
|
||||
|
||||
/**
|
||||
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
||||
*
|
||||
@@ -296,6 +320,7 @@ export const { fft } = registerControl('fft');
|
||||
*
|
||||
* @name decay
|
||||
* @param {number | Pattern} time decay time in seconds
|
||||
* @synonyms dec
|
||||
* @example
|
||||
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||
*
|
||||
@@ -434,6 +459,78 @@ export const { crush } = registerControl('crush');
|
||||
*/
|
||||
export const { coarse } = registerControl('coarse');
|
||||
|
||||
/**
|
||||
* modulate the amplitude of a sound with a continuous waveform
|
||||
*
|
||||
* @name tremolo
|
||||
* @synonyms trem
|
||||
* @param {number | Pattern} speed modulation speed in HZ
|
||||
* @example
|
||||
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
|
||||
*
|
||||
*/
|
||||
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||
|
||||
/**
|
||||
* modulate the amplitude of a sound with a continuous waveform
|
||||
*
|
||||
* @name tremolosync
|
||||
* @synonyms tremsync
|
||||
* @param {number | Pattern} cycles modulation speed in cycles
|
||||
* @example
|
||||
* note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>")
|
||||
*
|
||||
*/
|
||||
export const { tremolosync } = registerControl(
|
||||
['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'],
|
||||
'tremsync',
|
||||
);
|
||||
|
||||
/**
|
||||
* depth of amplitude modulation
|
||||
*
|
||||
* @name tremolodepth
|
||||
* @synonyms tremdepth
|
||||
* @param {number | Pattern} depth
|
||||
* @example
|
||||
* note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>")
|
||||
*
|
||||
*/
|
||||
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
||||
/**
|
||||
* alter the shape of the modulation waveform
|
||||
*
|
||||
* @name tremoloskew
|
||||
* @synonyms tremskew
|
||||
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
|
||||
* @example
|
||||
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>")
|
||||
*
|
||||
*/
|
||||
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
||||
|
||||
/**
|
||||
* alter the phase of the modulation waveform
|
||||
*
|
||||
* @name tremolophase
|
||||
* @synonyms tremphase
|
||||
* @param {number | Pattern} offset the offset in cycles of the modulation
|
||||
* @example
|
||||
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>")
|
||||
*
|
||||
*/
|
||||
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||
|
||||
/**
|
||||
* shape of amplitude modulation
|
||||
*
|
||||
* @name tremoloshape
|
||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||
* @example
|
||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||
*
|
||||
*/
|
||||
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||
/**
|
||||
* filter overdrive for supported filter types
|
||||
*
|
||||
@@ -443,6 +540,42 @@ export const { coarse } = registerControl('coarse');
|
||||
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* modulate the amplitude of an orbit to create a "sidechain" like effect
|
||||
*
|
||||
* @name duckorbit
|
||||
* @param {number | Pattern} orbit target orbit
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
|
||||
*
|
||||
*/
|
||||
export const { duck } = registerControl('duckorbit', 'duck');
|
||||
|
||||
/**
|
||||
* the amount of ducking applied to target orbit
|
||||
*
|
||||
* @name duckdepth
|
||||
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
||||
* @example
|
||||
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
|
||||
*
|
||||
*/
|
||||
|
||||
export const { duckdepth } = registerControl('duckdepth');
|
||||
|
||||
/**
|
||||
* the attack time of the duck effect
|
||||
*
|
||||
* @name duckattack
|
||||
* @param {number | Pattern} time
|
||||
* @example
|
||||
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
|
||||
*
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
@@ -975,15 +1108,31 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
|
||||
*
|
||||
*/
|
||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||
/* // TODO: test
|
||||
|
||||
/**
|
||||
* Sets the time of the delay effect in cycles.
|
||||
*
|
||||
* @name delaysync
|
||||
* @param {number | Pattern} cycles delay length in cycles
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8))
|
||||
*
|
||||
*/
|
||||
export const { delaysync } = registerControl('delaysync');
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @superdirtOnly
|
||||
* @example
|
||||
* s("sd").delay().lock(1).osc()
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const { lock } = registerControl('lock');
|
||||
/**
|
||||
* Set detune for stacked voices of supported oscillators
|
||||
@@ -1535,18 +1684,6 @@ export const { density } = registerControl('density');
|
||||
// ['modwheel'],
|
||||
export const { expression } = registerControl('expression');
|
||||
export const { sustainpedal } = registerControl('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()
|
||||
*
|
||||
*/
|
||||
export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp');
|
||||
export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr');
|
||||
|
||||
export const { fshift } = registerControl('fshift');
|
||||
export const { fshiftnote } = registerControl('fshiftnote');
|
||||
@@ -1623,7 +1760,6 @@ export const { zmod } = registerControl('zmod');
|
||||
// like crush but scaled differently
|
||||
export const { zcrush } = registerControl('zcrush');
|
||||
export const { zdelay } = registerControl('zdelay');
|
||||
export const { tremolo } = registerControl('tremolo');
|
||||
export const { zzfx } = registerControl('zzfx');
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
|
||||
export class Cyclist {
|
||||
constructor({
|
||||
@@ -67,6 +67,7 @@ export class Cyclist {
|
||||
// the following line is dumb and only here for backwards compatibility
|
||||
// see https://codeberg.org/uzu/strudel/pulls/1004
|
||||
const deadline = targetTime - phase;
|
||||
// this onTrigger has another signature
|
||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||
this.cps = hap.value.cps;
|
||||
@@ -75,7 +76,7 @@ export class Cyclist {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
errorLogger(e);
|
||||
onError?.(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ 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 { timeCat, register, silence } from './pattern.mjs';
|
||||
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
|
||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||
import Fraction, { lcm } from './fraction.mjs';
|
||||
|
||||
@@ -139,6 +139,14 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||
});
|
||||
|
||||
export const e = register('e', function (euc, pat) {
|
||||
if (!Array.isArray(euc)) {
|
||||
euc = [euc];
|
||||
}
|
||||
const [pulses, steps = pulses, rot = 0] = euc;
|
||||
return pat.struct(_euclidRot(pulses, steps, rot));
|
||||
});
|
||||
|
||||
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, rotation));
|
||||
});
|
||||
@@ -188,3 +196,26 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||
return _euclidLegato(pulses, steps, rotation, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* A 'euclid' variant with an additional parameter that morphs the resulting
|
||||
* rhythm from 0 (no morphing) to 1 (completely 'even'). For example
|
||||
* `sound("bd").euclidish(3,8,0)` would be the same as
|
||||
* `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the
|
||||
* same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a
|
||||
* groove somewhere between.
|
||||
* Inspired by the work of Malcom Braff.
|
||||
* @name euclidish
|
||||
* @synonyms eish
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
|
||||
* @example
|
||||
* sound("hh").euclidish(7,12,sine.slow(8))
|
||||
* .pan(sine.slow(8))
|
||||
*/
|
||||
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
||||
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
|
||||
return pat.struct(morphed).setSteps(steps);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
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 const strudelScope = {};
|
||||
|
||||
export const evalScope = async (...args) => {
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
@@ -18,6 +20,7 @@ export const evalScope = async (...args) => {
|
||||
modules.forEach((module) => {
|
||||
Object.entries(module).forEach(([name, value]) => {
|
||||
globalThis[name] = value;
|
||||
strudelScope[name] = value;
|
||||
});
|
||||
});
|
||||
return modules;
|
||||
|
||||
@@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
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.mjs';
|
||||
import { stringifyValues } from './util.mjs';
|
||||
|
||||
export class Hap {
|
||||
/*
|
||||
@@ -148,13 +149,7 @@ export class Hap {
|
||||
}
|
||||
|
||||
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
|
||||
}`;
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
|
||||
}
|
||||
|
||||
combineContext(b) {
|
||||
|
||||
@@ -4,6 +4,12 @@ let debounce = 1000,
|
||||
lastMessage,
|
||||
lastTime;
|
||||
|
||||
export function errorLogger(e, origin = 'cyclist') {
|
||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
||||
// console.error(e);
|
||||
logger(`[${origin}] error: ${e.message}`);
|
||||
}
|
||||
|
||||
export function logger(message, type, data = {}) {
|
||||
let t = performance.now();
|
||||
if (lastMessage === message && t - lastTime < debounce) {
|
||||
|
||||
@@ -11,7 +11,6 @@ export class NeoCyclist {
|
||||
constructor({ onTrigger, onToggle, getTime }) {
|
||||
this.started = false;
|
||||
this.cps = 0.5;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.time_at_last_tick_message = 0;
|
||||
// the clock of the worker and the audio context clock can drift apart over time
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/core",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
+149
-49
@@ -21,7 +21,8 @@ import {
|
||||
numeralArgs,
|
||||
parseNumeral,
|
||||
pairs,
|
||||
noteToMidi,
|
||||
zipWith,
|
||||
stringifyValues,
|
||||
} from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
@@ -853,14 +854,29 @@ export class Pattern {
|
||||
);
|
||||
}
|
||||
|
||||
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
|
||||
/**
|
||||
* Writes the content of the current event to the console (visible in the side menu).
|
||||
* @name log
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("bd sd").log()
|
||||
*/
|
||||
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
||||
return this.onTrigger((...args) => {
|
||||
logger(func(...args), undefined, getData(...args));
|
||||
}, false);
|
||||
}
|
||||
|
||||
logValues(func = id) {
|
||||
return this.log((_, hap) => func(hap.value));
|
||||
/**
|
||||
* A simplified version of `log` which writes all "values" (various configurable parameters)
|
||||
* within the event to the console (visible in the side menu).
|
||||
* @name logValues
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
|
||||
*/
|
||||
logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
|
||||
return this.log((hap) => func(hap.value));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -870,6 +886,31 @@ export class Pattern {
|
||||
console.log(drawLine(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// methods relating to breaking patterns into subcycles
|
||||
|
||||
// Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern.
|
||||
unjoin(pieces, func = id) {
|
||||
return pieces.withHap((hap) =>
|
||||
hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks a pattern into pieces according to the structure of a given pattern.
|
||||
* True values in the given pattern cause the corresponding subcycle of the
|
||||
* source pattern to be looped, and for an (optional) given function to be
|
||||
* applied. False values result in the corresponding part of the source pattern
|
||||
* to be played unchanged.
|
||||
* @name into
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt").into("1 0", hurry(2))
|
||||
*/
|
||||
into(pieces, func) {
|
||||
return this.unjoin(pieces, func).innerJoin();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -905,12 +946,13 @@ Pattern.prototype.collect = function () {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arpWith(haps => haps[2])
|
||||
* */
|
||||
Pattern.prototype.arpWith = function (func) {
|
||||
return this.collect()
|
||||
export const arpWith = register('arpWith', (func, pat) => {
|
||||
return pat
|
||||
.collect()
|
||||
.fmap((v) => reify(func(v)))
|
||||
.innerJoin()
|
||||
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Selects indices in in stacked notes.
|
||||
@@ -918,9 +960,11 @@ Pattern.prototype.arpWith = function (func) {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arp("0 [0,2] 1 [0,2]")
|
||||
* */
|
||||
Pattern.prototype.arp = function (pat) {
|
||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||
};
|
||||
export const arp = register(
|
||||
'arp',
|
||||
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
|
||||
false,
|
||||
);
|
||||
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
@@ -1261,7 +1305,7 @@ export function sequenceP(pats) {
|
||||
* @synonyms polyrhythm, pr
|
||||
* @example
|
||||
* stack("g3", "b3", ["e4", "d4"]).note()
|
||||
* // "g3,b3,[e4,d4]".note()
|
||||
* // "g3,b3,[e4 d4]".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
@@ -1542,7 +1586,7 @@ export const func = curry((a, b) => reify(b).func(a));
|
||||
/**
|
||||
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
|
||||
*
|
||||
* @param {string} name name of the function
|
||||
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
|
||||
* @param {function} func function with 1 or more params, where last is the current pattern
|
||||
* @noAutocomplete
|
||||
*
|
||||
@@ -1982,6 +2026,7 @@ export const apply = register('apply', function (func, pat) {
|
||||
|
||||
/**
|
||||
* Plays the pattern at the given cycles per minute.
|
||||
* @deprecated
|
||||
* @example
|
||||
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
|
||||
*/
|
||||
@@ -2491,6 +2536,37 @@ export const { fastchunk, fastChunk } = register(
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
|
||||
* @name chunkInto
|
||||
* @synonyms chunkinto
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||
* .bank("tr909")
|
||||
*/
|
||||
export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) {
|
||||
return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func);
|
||||
});
|
||||
|
||||
/**
|
||||
* Like `chunkInto`, but moves backwards through the chunks.
|
||||
* @name chunkBackInto
|
||||
* @synonyms chunkbackinto
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||
* .bank("tr909")
|
||||
*/
|
||||
export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) {
|
||||
return pat.into(
|
||||
fastcat(true, ...Array(n - 1).fill(false))
|
||||
._iter(n)
|
||||
._early(1),
|
||||
func,
|
||||
);
|
||||
});
|
||||
|
||||
// TODO - redefine elsewhere in terms of mask
|
||||
export const bypass = register(
|
||||
'bypass',
|
||||
@@ -2506,7 +2582,7 @@ export const bypass = register(
|
||||
* Loops the pattern inside an `offset` for `cycles`.
|
||||
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
|
||||
* @name ribbon
|
||||
* @synonym rib
|
||||
* @synonyms rib
|
||||
* @param {number} offset start point of loop in cycles
|
||||
* @param {number} cycles loop length in cycles
|
||||
* @example
|
||||
@@ -2720,7 +2796,7 @@ export function stepcat(...timepats) {
|
||||
if (timepats.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]);
|
||||
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]);
|
||||
timepats = timepats.map(findsteps);
|
||||
if (timepats.find((x) => x[0] === undefined)) {
|
||||
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
|
||||
@@ -3204,7 +3280,7 @@ export const slice = register(
|
||||
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
|
||||
*/
|
||||
Pattern.prototype.onTriggerTime = function (func) {
|
||||
return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
const diff = targetTime - currentTime;
|
||||
window.setTimeout(() => {
|
||||
func(hap);
|
||||
@@ -3342,44 +3418,68 @@ export const { beat } = register(
|
||||
__beat((x) => x.innerJoin()),
|
||||
);
|
||||
|
||||
const __quantizeBy = (lens, scale, pat) => {
|
||||
// Supports ':' list syntax in mininotation
|
||||
scale = (Array.isArray(scale) ? scale.flat() : [scale]).flatMap((val) =>
|
||||
typeof val === 'number' ? val : noteToMidi(val) - 48,
|
||||
export const _morph = (from, to, by) => {
|
||||
by = Fraction(by);
|
||||
const dur = Fraction(1).div(from.length);
|
||||
const positions = (list) => {
|
||||
const result = [];
|
||||
for (const [pos, value] of list.entries()) {
|
||||
if (value) {
|
||||
result.push([Fraction(pos).div(list.length), value]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const arcs = zipWith(
|
||||
([posa, valuea], [posb, valueb]) => {
|
||||
const b = by.mul(posb - posa).add(posa);
|
||||
const e = b.add(dur);
|
||||
return new TimeSpan(b, e);
|
||||
},
|
||||
positions(from),
|
||||
positions(to),
|
||||
);
|
||||
|
||||
return pat.withHap((hap) => {
|
||||
const isObject = typeof hap.value === 'object';
|
||||
let note = isObject ? hap.value.n : hap.value;
|
||||
if (typeof note === 'number') {
|
||||
note = note;
|
||||
function query(state) {
|
||||
const cycle = state.span.begin.sam();
|
||||
const cycleArc = state.span.cycleArc();
|
||||
const result = [];
|
||||
for (const whole of arcs) {
|
||||
const part = whole.intersection(cycleArc);
|
||||
if (part !== undefined) {
|
||||
result.push(
|
||||
new Hap(
|
||||
whole.withTime((x) => x.add(cycle)),
|
||||
part.withTime((x) => x.add(cycle)),
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note);
|
||||
}
|
||||
|
||||
if (isObject) {
|
||||
delete hap.value.n; // remove n so it won't cause trouble
|
||||
}
|
||||
const octave = (note / lens) >> 0;
|
||||
const transpose = octave * lens;
|
||||
|
||||
const goal = note - transpose;
|
||||
note =
|
||||
scale.reduce((prev, curr) => {
|
||||
return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev;
|
||||
}) + transpose;
|
||||
|
||||
return hap.withValue(() => (isObject ? { ...hap.value, note } : note));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return new Pattern(query).splitQueries();
|
||||
};
|
||||
|
||||
/**
|
||||
* Snap note values to a chosen array of notes within a 12 note/octave scale
|
||||
* @name quantize
|
||||
* Takes two binary rhythms represented as lists of 1s and 0s, and a number
|
||||
* between 0 and 1 that morphs between them. The two lists should contain the same
|
||||
* number of true values.
|
||||
* @example
|
||||
* note(irand(35).add(48).seg(16).quantize("d:a:a#:f")).s("pulse")
|
||||
* sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
|
||||
* [1,1,0,1,0,1,0], // wonky rhythm
|
||||
* 0.25 // creates a slightly wonky rhythm
|
||||
* )
|
||||
* )
|
||||
* @example
|
||||
* sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
|
||||
* "1:1:0:1:0:1:0", // wonky rhythm
|
||||
* sine.slow(8) // slowly morph between the rhythms
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
export const { quantize } = register(['quantize'], (scale, pat) => {
|
||||
return __quantizeBy(12, scale, pat);
|
||||
});
|
||||
export const morph = (frompat, topat, bypat) => {
|
||||
frompat = reify(frompat);
|
||||
topat = reify(topat);
|
||||
bypat = reify(bypat);
|
||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||
};
|
||||
|
||||
+27
-6
@@ -1,7 +1,7 @@
|
||||
import { NeoCyclist } from './neocyclist.mjs';
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
@@ -21,6 +21,7 @@ export function repl({
|
||||
setInterval,
|
||||
clearInterval,
|
||||
id,
|
||||
mondo = false,
|
||||
}) {
|
||||
const state = {
|
||||
schedulerError: undefined,
|
||||
@@ -85,6 +86,17 @@ export function repl({
|
||||
const pause = () => scheduler.pause();
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
|
||||
/**
|
||||
* Changes the global tempo to the given cycles per minute
|
||||
*
|
||||
* @name setcpm
|
||||
* @alias setCpm
|
||||
* @param {number} cpm cycles per minute
|
||||
* @example
|
||||
* setcpm(140/4) // =140 bpm in 4/4
|
||||
* $: s("bd*4,[- sd]*2").bank('tr707')
|
||||
*/
|
||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||
|
||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||
@@ -102,8 +114,9 @@ export function repl({
|
||||
* all(x => x.pianoroll())
|
||||
* ```
|
||||
*/
|
||||
let allTransforms = [];
|
||||
const all = function (transform) {
|
||||
allTransform = transform;
|
||||
allTransforms.push(transform);
|
||||
return silence;
|
||||
};
|
||||
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
|
||||
@@ -179,7 +192,12 @@ export function repl({
|
||||
await injectPatternMethods();
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
await beforeEval?.({ code });
|
||||
allTransforms = []; // reset all transforms
|
||||
shouldHush && hush();
|
||||
|
||||
if (mondo) {
|
||||
code = `mondolang\`${code}\``;
|
||||
}
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = Object.values(pPatterns);
|
||||
@@ -191,8 +209,10 @@ export function repl({
|
||||
} else if (eachTransform) {
|
||||
pattern = eachTransform(pattern);
|
||||
}
|
||||
if (allTransform) {
|
||||
pattern = allTransform(pattern);
|
||||
if (allTransforms.length) {
|
||||
for (let i in allTransforms) {
|
||||
pattern = allTransforms[i](pattern);
|
||||
}
|
||||
}
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
@@ -225,6 +245,7 @@ export function repl({
|
||||
export const getTrigger =
|
||||
({ getTime, defaultOutput }) =>
|
||||
async (hap, deadline, duration, cps, t) => {
|
||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
@@ -232,9 +253,9 @@ export const getTrigger =
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
|
||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
errorLogger(err, 'getTrigger');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,7 +71,6 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
|
||||
/**
|
||||
* A sine signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* n(sine.segment(16).range(0,15))
|
||||
@@ -100,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
|
||||
/**
|
||||
* A square signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||
@@ -266,7 +264,7 @@ export const randrun = (n) => {
|
||||
const rands = timeToRands(t.floor().add(0.5), n);
|
||||
const nums = rands
|
||||
.map((n, i) => [n, i])
|
||||
.sort((a, b) => a[0] > b[0] - a[0] < b[0])
|
||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||
.map((x) => x[1]);
|
||||
const i = t.cyclePos().mul(n).floor() % n;
|
||||
return nums[i];
|
||||
@@ -285,7 +283,7 @@ const _rearrangeWith = (ipat, n, pat) => {
|
||||
* @example
|
||||
* note("c d e f").sound("piano").shuffle(4)
|
||||
* @example
|
||||
* note("c d e f".shuffle(4), "g").sound("piano")
|
||||
* seq("c d e f".shuffle(4), "g").note().sound("piano")
|
||||
*/
|
||||
export const shuffle = register('shuffle', (n, pat) => {
|
||||
return _rearrangeWith(randrun(n), n, pat);
|
||||
@@ -298,7 +296,7 @@ export const shuffle = register('shuffle', (n, pat) => {
|
||||
* @example
|
||||
* note("c d e f").sound("piano").scramble(4)
|
||||
* @example
|
||||
* note("c d e f".scramble(4), "g").sound("piano")
|
||||
* seq("c d e f".scramble(4), "g").note().sound("piano")
|
||||
*/
|
||||
export const scramble = register('scramble', (n, pat) => {
|
||||
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
||||
@@ -398,6 +396,10 @@ export const chooseInWith = (pat, xs) => {
|
||||
*/
|
||||
export const choose = (...xs) => chooseWith(rand, xs);
|
||||
|
||||
// todo: doc
|
||||
export const chooseIn = (...xs) => chooseInWith(rand, xs);
|
||||
export const chooseOut = choose;
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
|
||||
}
|
||||
|
||||
export const speak = register('speak', function (lang, voice, pat) {
|
||||
return pat.onTrigger((_, hap) => {
|
||||
return pat.onTrigger((hap) => {
|
||||
triggerSpeech(hap.value, lang, voice);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import Fraction from 'fraction.js';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
TimeSpan,
|
||||
@@ -52,8 +52,11 @@ import {
|
||||
stackCentre,
|
||||
stepcat,
|
||||
sometimes,
|
||||
expand,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { log, logValues } from '../pattern.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
import { n, s } from '../controls.mjs';
|
||||
@@ -1001,7 +1004,7 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('hurry', () => {
|
||||
it('Can speed up patterns and sounds', () => {
|
||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||
sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2));
|
||||
});
|
||||
});
|
||||
/*describe('composable functions', () => {
|
||||
@@ -1179,6 +1182,9 @@ describe('Pattern', () => {
|
||||
it('calculates undefined steps as the average', () => {
|
||||
expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3)));
|
||||
});
|
||||
it('works with auto-reified values', () => {
|
||||
expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim'))));
|
||||
});
|
||||
});
|
||||
describe('shrink', () => {
|
||||
it('can shrink', () => {
|
||||
@@ -1267,4 +1273,75 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('unjoin', () => {
|
||||
it('destructures a pattern into subcycles', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c', 'd')
|
||||
.unjoin(fastcat(true, fastcat(true, true)))
|
||||
.fmap(fast(2))
|
||||
.join(),
|
||||
fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('into', () => {
|
||||
it('applies a function to subcycles of a pattern', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)),
|
||||
fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chunkinto', () => {
|
||||
it('chunks into subcycles', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3),
|
||||
fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chunkbackinto', () => {
|
||||
it('chunks into subcycles backwards', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3),
|
||||
fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('log', () => {
|
||||
it('logs to console', () => {
|
||||
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const pattern = pure('a').log();
|
||||
const haps = pattern.queryArc(0, 1);
|
||||
|
||||
// Force a trigger
|
||||
haps.forEach((hap) => {
|
||||
hap.context?.onTrigger?.(hap);
|
||||
});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||
'%c[hap] 0/1 → 1/1: a',
|
||||
'background-color: black;color:white;border-radius:15px',
|
||||
);
|
||||
mockConsoleLog.mockRestore();
|
||||
});
|
||||
});
|
||||
describe('logValues', () => {
|
||||
it('logs values to console', () => {
|
||||
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const pattern = pure('a').note('c#').logValues();
|
||||
const haps = pattern.queryArc(0, 1);
|
||||
|
||||
// Force a trigger
|
||||
haps.forEach((hap) => {
|
||||
hap.context?.onTrigger?.(hap);
|
||||
});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||
'%c[hap] value:a note:c#',
|
||||
'background-color: black;color:white;border-radius:15px',
|
||||
);
|
||||
mockConsoleLog.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ export class TimeSpan {
|
||||
}
|
||||
|
||||
intersection(other) {
|
||||
// Intersection of two timespans, returns None if they don't intersect.
|
||||
// Intersection of two timespans, returns undefined if they don't intersect.
|
||||
const intersect_begin = this.begin.max(other.begin);
|
||||
const intersect_end = this.end.min(other.end);
|
||||
|
||||
|
||||
@@ -487,3 +487,13 @@ export function getCurrentKeyboardState() {
|
||||
// }
|
||||
// return lcm((x * y) / gcd(x, y), ...z);
|
||||
// };
|
||||
|
||||
// Takes values -- typically derived from events, i.e. `hap`s -- and renders them
|
||||
// into a readable format
|
||||
export function stringifyValues(value, compact = false) {
|
||||
return typeof value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(value)
|
||||
: value;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
||||
instrument = instrument || 'triangle';
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
|
||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
@@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
p1 = `"${instrument}"`;
|
||||
}
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
return pat.onTrigger((tidal_time, hap) => {
|
||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
@@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
throw new Error('csound only support objects as hap values');
|
||||
}
|
||||
// Time in seconds counting from now.
|
||||
const p2 = tidal_time - getAudioContext().currentTime;
|
||||
const p2 = targetTime - currentTime;
|
||||
const p3 = hap.duration.valueOf() + 0;
|
||||
const frequency = getFrequency(hap);
|
||||
let { gain = 1, velocity = 0.9 } = hap.value;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80;
|
||||
const CC_MESSAGE = 0xb0;
|
||||
|
||||
Pattern.prototype.midi = function (output) {
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||
const latencyMs = 34;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs';
|
||||
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const params = [];
|
||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||
|
||||
@@ -150,7 +150,7 @@ export class Drawer {
|
||||
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.endClipped >= phase - lookbehind - lookahead)
|
||||
.filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead)
|
||||
// add new haps with onset (think right edge bars scrolling in)
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
const time = phase - lookahead;
|
||||
@@ -175,7 +175,7 @@ export class Drawer {
|
||||
// +0.1 = workaround for weird holes in query..
|
||||
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);
|
||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t);
|
||||
this.painters = []; // will get populated by .onPaint calls attached to the pattern
|
||||
// query future haps
|
||||
const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/draw",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Helpers for drawing with Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/embed",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -56,14 +56,15 @@ You can use button inputs to control different aspects of your music, such as ga
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use button values to control amplitude
|
||||
setcpm(120)
|
||||
// Use button values to control amplitude
|
||||
$: stack(
|
||||
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
|
||||
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
|
||||
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
|
||||
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
|
||||
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
|
||||
).cpm(120)
|
||||
)
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -74,14 +75,13 @@ Analog sticks can be used for continuous control, such as pitch shifting or pann
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use analog stick for continuous control
|
||||
setcpm(120)
|
||||
// Use analog stick for continuous control
|
||||
$: note("c4 d3 a3 e3").sound("sawtooth")
|
||||
.lpf(gp.x1.range(100,4000))
|
||||
.lpq(gp.y1.range(5,30))
|
||||
.decay(gp.y2.range(0.1,2))
|
||||
.lpenv(gp.x2.range(-5,5))
|
||||
.cpm(120)
|
||||
`}
|
||||
.lpenv(gp.x2.range(-5,5))`}
|
||||
/>
|
||||
|
||||
### Button Sequences
|
||||
@@ -89,6 +89,7 @@ $: note("c4 d3 a3 e3").sound("sawtooth")
|
||||
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
|
||||
|
||||
<MiniRepl client:idle tune={`const gp = gamepad(0)
|
||||
setcpm(120)
|
||||
// Define button sequences
|
||||
const HADOUKEN = [
|
||||
'd', // Down
|
||||
@@ -99,7 +100,7 @@ const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
|
||||
|
||||
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
|
||||
$: s("free_hadouken -").slow(2)
|
||||
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
|
||||
.mask(gp.btnSequence(HADOUKEN)).room(1)
|
||||
|
||||
// hadouken.wav by Syna-Max
|
||||
//https://freesound.org/people/Syna-Max/sounds/67674/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Hydra integration for strudel",
|
||||
"main": "hydra.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -333,7 +333,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||
});
|
||||
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||
if (!WebMidi.enabled) {
|
||||
logger('Midi not enabled');
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# mondo
|
||||
|
||||
a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet.
|
||||
see the `test` folder for usage examples
|
||||
|
||||
more info:
|
||||
|
||||
- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html)
|
||||
- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html)
|
||||
|
||||
## Example Usage
|
||||
|
||||
```js
|
||||
import { MondoRunner } from 'mondolang'
|
||||
// define our library of functions and variables
|
||||
let lib = {
|
||||
add: (a, b) => a + b,
|
||||
mul: (a, b) => a * b,
|
||||
PI: Math.PI,
|
||||
};
|
||||
// this function will evaluate nodes in the syntax tree
|
||||
function evaluator(node) {
|
||||
// check if node is a leaf node (!= list)
|
||||
if (node.type !== 'list') {
|
||||
// check lib if we find a match in the lib, otherwise return value
|
||||
return lib[node.value] ?? node.value;
|
||||
}
|
||||
// now it can only be a list..
|
||||
const [fn, ...args] = node.children;
|
||||
// children in a list will already be evaluated
|
||||
// the first child is expected to be a function
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`"${fn}" is not a function`);
|
||||
}
|
||||
return fn(...args);
|
||||
}
|
||||
const runner = new MondoRunner({ evaluator });
|
||||
const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586
|
||||
```
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
mondo.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.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/>.
|
||||
*/
|
||||
|
||||
// evolved from https://garten.salat.dev/lisp/parser.html
|
||||
export class MondoParser {
|
||||
// these are the tokens we expect
|
||||
token_types = {
|
||||
comment: /^\/\/(.*?)(?=\n|$)/,
|
||||
quotes_double: /^"(.*?)"/,
|
||||
quotes_single: /^'(.*?)'/,
|
||||
open_list: /^\(/,
|
||||
close_list: /^\)/,
|
||||
open_angle: /^</,
|
||||
close_angle: /^>/,
|
||||
open_square: /^\[/,
|
||||
close_square: /^\]/,
|
||||
open_curly: /^\{/,
|
||||
close_curly: /^\}/,
|
||||
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
|
||||
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
|
||||
op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
|
||||
// dollar: /^\$/,
|
||||
pipe: /^#/,
|
||||
stack: /^[,$]/,
|
||||
or: /^[|]/,
|
||||
plain: /^[a-zA-Z0-9-~_^#]+/,
|
||||
};
|
||||
// matches next token
|
||||
next_token(code, offset = 0) {
|
||||
for (let type in this.token_types) {
|
||||
const match = code.match(this.token_types[type]);
|
||||
if (match) {
|
||||
let token = { type, value: match[0] };
|
||||
if (offset !== -1) {
|
||||
// add location
|
||||
token.loc = [offset, offset + match[0].length];
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
throw new Error(`mondo: could not match '${code}'`);
|
||||
}
|
||||
// takes code string, returns list of matched tokens (if valid)
|
||||
tokenize(code, offset = 0) {
|
||||
let tokens = [];
|
||||
let locEnabled = offset !== -1;
|
||||
let trim = () => {
|
||||
// trim whitespace at start, update offset
|
||||
offset += code.length - code.trimStart().length;
|
||||
// trim start and end to not confuse parser
|
||||
return code.trim();
|
||||
};
|
||||
code = trim();
|
||||
while (code.length > 0) {
|
||||
code = trim();
|
||||
const token = this.next_token(code, locEnabled ? offset : -1);
|
||||
code = code.slice(token.value.length);
|
||||
offset += token.value.length;
|
||||
tokens.push(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
// take code, return abstract syntax tree
|
||||
parse(code, offset) {
|
||||
this.code = code;
|
||||
this.offset = offset;
|
||||
this.tokens = this.tokenize(code, offset);
|
||||
const expressions = [];
|
||||
while (this.tokens.length) {
|
||||
expressions.push(this.parse_expr());
|
||||
}
|
||||
if (expressions.length === 0) {
|
||||
// empty case
|
||||
return { type: 'list', children: [] };
|
||||
}
|
||||
// do we have multiple top level expressions or a single non list?
|
||||
if (expressions.length > 1 || expressions[0].type !== 'list') {
|
||||
return {
|
||||
type: 'list',
|
||||
children: this.desugar(expressions),
|
||||
};
|
||||
}
|
||||
// we have a single list
|
||||
return expressions[0];
|
||||
}
|
||||
// parses any valid expression
|
||||
parse_expr() {
|
||||
if (!this.tokens[0]) {
|
||||
throw new Error(`unexpected end of file`);
|
||||
// TODO: could we allow that? like (((((((( s bd
|
||||
// return { type: 'list', children: [] };
|
||||
}
|
||||
let next = this.tokens[0]?.type;
|
||||
if (next === 'open_list') {
|
||||
return this.parse_list();
|
||||
}
|
||||
if (next === 'open_angle') {
|
||||
return this.parse_angle();
|
||||
}
|
||||
if (next === 'open_square') {
|
||||
return this.parse_square();
|
||||
}
|
||||
if (next === 'open_curly') {
|
||||
return this.parse_curly();
|
||||
}
|
||||
return this.consume(next);
|
||||
}
|
||||
// Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']]
|
||||
split_children(children, split_type) {
|
||||
const chunks = [];
|
||||
while (true) {
|
||||
let splitIndex = children.findIndex((child) => child.type === split_type);
|
||||
if (splitIndex === -1) break;
|
||||
const chunk = children.slice(0, splitIndex);
|
||||
chunks.push(chunk);
|
||||
children = children.slice(splitIndex + 1);
|
||||
}
|
||||
chunks.push(children);
|
||||
return chunks;
|
||||
}
|
||||
desugar_split(children, split_type, next) {
|
||||
const chunks = this.split_children(children, split_type);
|
||||
if (chunks.length === 1) {
|
||||
return next(children);
|
||||
}
|
||||
// collect args of stack function
|
||||
const args = chunks
|
||||
.map((chunk) => {
|
||||
if (!chunk.length) {
|
||||
return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty)
|
||||
}
|
||||
if (chunk.length === 1) {
|
||||
// chunks of one element can be added to the stack as is
|
||||
return chunk[0];
|
||||
}
|
||||
// chunks of multiple args
|
||||
chunk = next(chunk);
|
||||
return { type: 'list', children: chunk };
|
||||
})
|
||||
.filter(Boolean); // ignore empty chunks
|
||||
return [{ type: 'plain', value: split_type }, ...args];
|
||||
}
|
||||
// prevents to get a list, e.g. ((x y)) => (x y)
|
||||
unwrap_children(children) {
|
||||
if (children.length === 1) {
|
||||
return children[0].children;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
desugar_ops(children) {
|
||||
while (true) {
|
||||
let opIndex = children.findIndex((child) => child.type === 'op');
|
||||
if (opIndex === -1) break;
|
||||
const op = { type: 'plain', value: children[opIndex].value };
|
||||
if (opIndex === children.length - 1) {
|
||||
//throw new Error(`cannot use operator as last child.`);
|
||||
children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]"
|
||||
continue;
|
||||
}
|
||||
if (opIndex === 0) {
|
||||
// regular function call (assuming each operator exists as function)
|
||||
children[opIndex] = op;
|
||||
continue;
|
||||
}
|
||||
// convert infix to prefix notation
|
||||
const left = children[opIndex - 1];
|
||||
const right = children[opIndex + 1];
|
||||
if (left.type === 'pipe') {
|
||||
// "x !* 2" => (* 2 x)
|
||||
children[opIndex] = op;
|
||||
continue;
|
||||
}
|
||||
// some careful error handling
|
||||
if (left.type === 'op') {
|
||||
throw new Error(`got 2 ops in a row: "${left.value}${op.value}"`);
|
||||
}
|
||||
if (right.type === 'op') {
|
||||
let err = `got 2 ops in a row: "${op.value}${right.value}"`;
|
||||
if (op.value === '-') {
|
||||
// yes i know this file is not supposed to know about rests x.X
|
||||
err += '. you probably want a rest, which is "_" in mondo!';
|
||||
}
|
||||
throw new Error(err);
|
||||
}
|
||||
const call = { type: 'list', children: [op, right, left] };
|
||||
// insert call while keeping other siblings
|
||||
children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)];
|
||||
children = this.unwrap_children(children);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
get_lambda(args, children) {
|
||||
// (.fast 2) = (fn (_) (fast _ 2))
|
||||
children = this.desugar(children);
|
||||
const body = children.length === 1 ? children[0] : { type: 'list', children };
|
||||
return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body];
|
||||
}
|
||||
// returns location range of given ast (even if desugared)
|
||||
get_range(ast, range = [Infinity, 0]) {
|
||||
let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])];
|
||||
if (ast.loc) {
|
||||
return union(range, ast.loc);
|
||||
}
|
||||
if (ast.type !== 'list') {
|
||||
return range;
|
||||
}
|
||||
return ast.children.reduce((range, child) => {
|
||||
const childrange = this.get_range(child, range);
|
||||
return union(range, childrange);
|
||||
}, range);
|
||||
}
|
||||
errorhead(ast) {
|
||||
return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`;
|
||||
}
|
||||
// returns original user code where the given ast originates (even if desugared)
|
||||
get_code_snippet(ast) {
|
||||
const [min, max] = this.get_range(ast);
|
||||
return this.code.slice(min - this.offset, max - this.offset);
|
||||
}
|
||||
desugar_pipes(children) {
|
||||
let chunks = this.split_children(children, 'pipe');
|
||||
while (chunks.length > 1) {
|
||||
let [left, right, ...rest] = chunks;
|
||||
|
||||
if (!left.length) {
|
||||
const arg = { type: 'plain', value: '_' };
|
||||
return this.get_lambda([arg], [arg, ...children]);
|
||||
}
|
||||
// s jazz hh.fast 2 => (fast 2 (s jazz hh))
|
||||
const call = left.length > 1 ? { type: 'list', children: left } : left[0];
|
||||
chunks = [[...right, call], ...rest];
|
||||
}
|
||||
// return next(chunks[0]);
|
||||
return chunks[0];
|
||||
}
|
||||
parse_pair(open_type, close_type) {
|
||||
const begin = this.tokens[0].loc?.[0];
|
||||
this.consume(open_type);
|
||||
const children = [];
|
||||
while (this.tokens[0]?.type !== close_type) {
|
||||
children.push(this.parse_expr());
|
||||
}
|
||||
const end = this.tokens[0].loc?.[1];
|
||||
this.consume(close_type);
|
||||
const node = { type: 'list', children };
|
||||
if (begin !== undefined) {
|
||||
node.loc = [begin, end];
|
||||
node.raw = this.code.slice(begin, end);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
desugar(children, type) {
|
||||
// if type is given, the first element is expected to contain it as plain value
|
||||
// e.g. with (square a b, c), we want to split (a b, c) and ignore "square"
|
||||
children = type ? children.slice(1) : children;
|
||||
children = this.desugar_split(children, 'stack', (children) =>
|
||||
this.desugar_split(children, 'or', (children) => {
|
||||
// chunks of multiple args
|
||||
if (type) {
|
||||
// the type we've removed before splitting needs to be added back
|
||||
children = [{ type: 'plain', value: type }, ...children];
|
||||
}
|
||||
children = this.desugar_ops(children);
|
||||
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
|
||||
children = this.desugar_pipes(children);
|
||||
return children;
|
||||
}),
|
||||
);
|
||||
return children;
|
||||
}
|
||||
parse_list() {
|
||||
let node = this.parse_pair('open_list', 'close_list');
|
||||
node.children = this.desugar(node.children);
|
||||
return node;
|
||||
}
|
||||
parse_angle() {
|
||||
let node = this.parse_pair('open_angle', 'close_angle');
|
||||
node.children.unshift({ type: 'plain', value: 'angle' });
|
||||
node.children = this.desugar(node.children, 'angle');
|
||||
return node;
|
||||
}
|
||||
parse_square() {
|
||||
let node = this.parse_pair('open_square', 'close_square');
|
||||
node.children.unshift({ type: 'plain', value: 'square' });
|
||||
node.children = this.desugar(node.children, 'square');
|
||||
return node;
|
||||
}
|
||||
parse_curly() {
|
||||
let node = this.parse_pair('open_curly', 'close_curly');
|
||||
node.children.unshift({ type: 'plain', value: 'curly' });
|
||||
node.children = this.desugar(node.children, 'curly');
|
||||
return node;
|
||||
}
|
||||
consume(type) {
|
||||
// shift removes first element and returns it
|
||||
const token = this.tokens.shift();
|
||||
if (token.type !== type) {
|
||||
throw new Error(`expected token type ${type}, got ${token.type}`);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
get_locations(code, offset = 0) {
|
||||
let walk = (ast, locations = []) => {
|
||||
if (ast.type === 'list') {
|
||||
return ast.children.forEach((child) => walk(child, locations));
|
||||
}
|
||||
if (ast.loc) {
|
||||
locations.push(ast.loc);
|
||||
}
|
||||
};
|
||||
const ast = this.parse(code, offset);
|
||||
let locations = [];
|
||||
walk(ast, locations);
|
||||
return locations;
|
||||
}
|
||||
}
|
||||
|
||||
export function printAst(ast, compact = false, lvl = 0) {
|
||||
const br = compact ? '' : '\n';
|
||||
const spaces = compact ? '' : Array(lvl).fill(' ').join('');
|
||||
if (ast.type === 'list') {
|
||||
return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${
|
||||
ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')'
|
||||
}`;
|
||||
}
|
||||
return `${ast.value}`;
|
||||
}
|
||||
|
||||
// lisp runner
|
||||
export class MondoRunner {
|
||||
constructor({ evaluator } = {}) {
|
||||
this.parser = new MondoParser();
|
||||
this.evaluator = evaluator;
|
||||
this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`);
|
||||
}
|
||||
// a helper to check conditions and throw if they are not met
|
||||
assert(condition, error) {
|
||||
if (!condition) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
run(code, scope, offset = 0) {
|
||||
const ast = this.parser.parse(code, offset);
|
||||
//console.log(printAst(ast));
|
||||
return this.evaluate(ast, scope);
|
||||
}
|
||||
evaluate_let(ast, scope) {
|
||||
// (let ((x 3) (y 4)) ...body)
|
||||
// = ((fn (x y) ...body) 3 4)
|
||||
const defs = ast.children[1].children;
|
||||
const args = defs.map((pair) => pair.children[0]);
|
||||
const vals = defs.map((pair) => pair.children[1]);
|
||||
const body = ast.children.slice(2);
|
||||
const lambda = {
|
||||
type: 'list',
|
||||
children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body],
|
||||
};
|
||||
return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope);
|
||||
}
|
||||
evaluate_def(ast, scope) {
|
||||
// function definition special form?
|
||||
if (ast.children[1].type === 'list') {
|
||||
// (def (add a b) (+ a b))
|
||||
// => (def add (fn (a b) (+ a b)) )
|
||||
const args = ast.children[1].children.slice(1);
|
||||
const lambda = {
|
||||
// lambda
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'fn' },
|
||||
{ type: 'list', children: args },
|
||||
...ast.children.slice(2), // body
|
||||
],
|
||||
};
|
||||
// we mutate to make sure the old ast wont make a mess later
|
||||
ast.children[1] = ast.children[1].children[0];
|
||||
ast.children[2] = lambda;
|
||||
ast.children = ast.children.slice(0, 3); // throw away rest
|
||||
}
|
||||
// (def name body)
|
||||
if (ast.children.length !== 3) {
|
||||
throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`);
|
||||
}
|
||||
const name = ast.children[1].value;
|
||||
const body = this.evaluate(ast.children[2], scope);
|
||||
scope[name] = body;
|
||||
// def with fall through
|
||||
}
|
||||
evaluate_match(ast, scope) {
|
||||
// (match (p1 e1) (p2 e2) ... (pn en))
|
||||
// = cond in lisp
|
||||
if (ast.children.length < 2) {
|
||||
return;
|
||||
}
|
||||
const [_, ...body] = ast.children;
|
||||
for (let i = 0; i < body.length; ++i) {
|
||||
const [predicate, exp] = body[i].children;
|
||||
if (predicate.value === 'else') {
|
||||
return this.evaluate(exp, scope);
|
||||
}
|
||||
const outcome = this.evaluate(predicate, scope);
|
||||
if (outcome) {
|
||||
return this.evaluate(exp, scope);
|
||||
}
|
||||
}
|
||||
return undefined; // nothing was matched
|
||||
}
|
||||
evaluate_if(ast, scope) {
|
||||
// if is a special case of match
|
||||
if (ast.children.length !== 4) {
|
||||
return;
|
||||
}
|
||||
// (if predicate consequent alternative)
|
||||
const [_, predicate, consequent, alternative] = ast.children;
|
||||
// (match (predicate consequent) (else alternative))
|
||||
const matcher = {
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'match' },
|
||||
{ type: 'list', children: [predicate, consequent] },
|
||||
{ type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] },
|
||||
],
|
||||
};
|
||||
return this.evaluate_match(matcher, scope);
|
||||
}
|
||||
evaluate_lambda(ast, scope) {
|
||||
// (fn (_) (ply 2 _)
|
||||
// ^args ^ body
|
||||
const [_, formalArgs, ...body] = ast.children;
|
||||
return (...args) => {
|
||||
const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]]));
|
||||
const closure = {
|
||||
...scope,
|
||||
...params,
|
||||
};
|
||||
// body can have multiple expressions
|
||||
const res = body.map((exp) => this.evaluate(exp, closure));
|
||||
// last expression is the return value
|
||||
return res[res.length - 1];
|
||||
};
|
||||
}
|
||||
evaluate_list(ast, scope) {
|
||||
// evaluate all children before evaluating list (dont mutate!!!)
|
||||
const args = ast.children
|
||||
.filter((child) => child.type !== 'comment') // ignore comments
|
||||
.map((arg) => this.evaluate(arg, scope));
|
||||
const node = { type: 'list', children: args };
|
||||
return this.evaluator(node, scope);
|
||||
}
|
||||
evaluate_leaf(ast, scope) {
|
||||
if (ast.type === 'number') {
|
||||
ast.value = Number(ast.value);
|
||||
} else if (['quotes_double', 'quotes_single'].includes(ast.type)) {
|
||||
ast.value = ast.value.slice(1, -1);
|
||||
ast.type = 'string';
|
||||
}
|
||||
return this.evaluator(ast, scope);
|
||||
}
|
||||
evaluate(ast, scope = {}) {
|
||||
if (ast.type !== 'list') {
|
||||
return this.evaluate_leaf(ast, scope);
|
||||
}
|
||||
const name = ast.children[0]?.value;
|
||||
if (name === 'fn') {
|
||||
return this.evaluate_lambda(ast, scope);
|
||||
}
|
||||
if (name === 'match') {
|
||||
return this.evaluate_match(ast, scope);
|
||||
}
|
||||
if (name === 'if') {
|
||||
return this.evaluate_if(ast, scope);
|
||||
}
|
||||
if (name === 'let') {
|
||||
return this.evaluate_let(ast, scope);
|
||||
}
|
||||
if (name === 'def') {
|
||||
this.evaluate_def(ast, scope);
|
||||
}
|
||||
return this.evaluate_list(ast, scope);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "mondolang",
|
||||
"version": "1.1.1",
|
||||
"description": "a language for functional composition that translates to js",
|
||||
"main": "mondo.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/mondo.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"bench": "vitest bench",
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/mondo/README.md",
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
/*
|
||||
mondo.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MondoParser, printAst, MondoRunner } from '../mondo.mjs';
|
||||
|
||||
const parser = new MondoParser();
|
||||
const p = (code) => parser.parse(code, -1);
|
||||
|
||||
describe('mondo tokenizer', () => {
|
||||
const parser = new MondoParser();
|
||||
it('should tokenize with locations', () =>
|
||||
expect(
|
||||
parser
|
||||
.tokenize('(one two three)')
|
||||
.map((t) => t.value + '=' + t.loc.join('-'))
|
||||
.join(' '),
|
||||
).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15'));
|
||||
// it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual());
|
||||
it('should get loangleions', () =>
|
||||
expect(parser.get_locations('s bd rim')).toEqual([
|
||||
[0, 1],
|
||||
[2, 4],
|
||||
[5, 8],
|
||||
]));
|
||||
});
|
||||
describe('mondo s-expressions parser', () => {
|
||||
it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] }));
|
||||
it('should parse a single item', () =>
|
||||
expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
|
||||
it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] }));
|
||||
it('should parse a list with 1 item', () =>
|
||||
expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
|
||||
it('should parse a list with 2 items', () =>
|
||||
expect(p('(a b)')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{ type: 'plain', value: 'b' },
|
||||
],
|
||||
}));
|
||||
it('should parse a list with 2 items', () =>
|
||||
expect(p('(a (b c))')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'b' },
|
||||
{ type: 'plain', value: 'c' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
it('should parse numbers', () =>
|
||||
expect(p('(1 .2 1.2 10 22.3)')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'number', value: '1' },
|
||||
{ type: 'number', value: '.2' },
|
||||
{ type: 'number', value: '1.2' },
|
||||
{ type: 'number', value: '10' },
|
||||
{ type: 'number', value: '22.3' },
|
||||
],
|
||||
}));
|
||||
it('should parse comments', () =>
|
||||
expect(p('a // hello')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{ type: 'comment', value: '// hello' },
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
let desguar = (a) => {
|
||||
return printAst(parser.parse(a), true);
|
||||
};
|
||||
|
||||
describe('mondo sugar', () => {
|
||||
it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)'));
|
||||
it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)'));
|
||||
it('should desugar <>', () => expect(desguar('<a b c>')).toEqual('(angle a b c)'));
|
||||
it('should desugar <> nested', () => expect(desguar('<a <b c> d>')).toEqual('(angle a (angle b c) d)'));
|
||||
it('should desugar mixed [] <>', () => expect(desguar('[a <b c>]')).toEqual('(square a (angle b c))'));
|
||||
it('should desugar mixed <> []', () => expect(desguar('<a [b c]>')).toEqual('(angle a (square b c))'));
|
||||
|
||||
it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))'));
|
||||
it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
|
||||
it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))'));
|
||||
it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))'));
|
||||
it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
|
||||
it('should desugar # within , within []', () =>
|
||||
expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)'));
|
||||
|
||||
// it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))'));
|
||||
|
||||
it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))'));
|
||||
it('should desugar , | of []', () =>
|
||||
expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))'));
|
||||
it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)'));
|
||||
it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))'));
|
||||
it('should desugar , square 3', () =>
|
||||
expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))'));
|
||||
it('should desugar , angle', () => expect(desguar('<bd, hh>')).toEqual('(stack bd hh)'));
|
||||
it('should desugar , angle 2', () => expect(desguar('<bd, hh oh>')).toEqual('(stack bd (angle hh oh))'));
|
||||
it('should desugar , angle 3', () =>
|
||||
expect(desguar('<bd cp, hh oh>')).toEqual('(stack (angle bd cp) (angle hh oh))'));
|
||||
it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))'));
|
||||
it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)'));
|
||||
it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))'));
|
||||
it('should desugar []*<x y>', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)'));
|
||||
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
|
||||
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
|
||||
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
|
||||
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
|
||||
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
|
||||
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
|
||||
it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */
|
||||
|
||||
it('should desugar README example', () =>
|
||||
expect(desguar('s [bd hh*2 (cp # crush 4) <mt ht lt>] # speed .8')).toEqual(
|
||||
'(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))',
|
||||
));
|
||||
|
||||
it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)'));
|
||||
it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))'));
|
||||
it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)'));
|
||||
it('should desugar lambda with pipe', () =>
|
||||
expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))'));
|
||||
/* const lambda = parser.parse('(lambda (_) (fast 2 _))');
|
||||
const target = { type: 'plain', value: 'xyz' };
|
||||
it('should desugar_lambda', () =>
|
||||
expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */
|
||||
});
|
||||
|
||||
describe('mondo arithmetic', () => {
|
||||
let multi =
|
||||
(op) =>
|
||||
(init, ...rest) =>
|
||||
rest.reduce((acc, arg) => op(acc, arg), init);
|
||||
|
||||
let lib = {
|
||||
'+': multi((a, b) => a + b),
|
||||
add: multi((a, b) => a + b),
|
||||
'-': multi((a, b) => a - b),
|
||||
sub: multi((a, b) => a - b),
|
||||
'*': multi((a, b) => a * b),
|
||||
'/': multi((a, b) => a / b),
|
||||
mod: multi((a, b) => a % b),
|
||||
eq: (a, b) => a === b,
|
||||
lt: (a, b) => a < b,
|
||||
gt: (a, b) => a > b,
|
||||
and: (a, b) => a && b,
|
||||
or: (a, b) => a || b,
|
||||
not: (a) => !a,
|
||||
run: (...args) => args[args.length - 1],
|
||||
def: () => 0,
|
||||
sin: Math.sin,
|
||||
cos: Math.cos,
|
||||
PI: Math.PI,
|
||||
cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])],
|
||||
car: (pair) => pair[0],
|
||||
cdr: (pair) => pair.slice(1),
|
||||
list: (...items) => items,
|
||||
nil: [],
|
||||
isnull: (items) => items.length === 0,
|
||||
concat: (...msgs) => msgs.join(''),
|
||||
error: (...msgs) => {
|
||||
throw new Error(msgs.join(' '));
|
||||
},
|
||||
};
|
||||
function evaluator(node, scope) {
|
||||
if (node.type !== 'list') {
|
||||
// is leaf
|
||||
return scope[node.value] ?? lib[node.value] ?? node.value;
|
||||
}
|
||||
// is list
|
||||
const [fn, ...args] = node.children;
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`);
|
||||
}
|
||||
return fn(...args);
|
||||
}
|
||||
const runner = new MondoRunner({ evaluator });
|
||||
let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope);
|
||||
let pretty = (exp) => printAst(runner.parser.parse(exp), false);
|
||||
//it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28'));
|
||||
|
||||
it('eval number', () => expect(evaluate('2')).toEqual(2));
|
||||
it('eval string', () => expect(evaluate('abc')).toEqual('abc'));
|
||||
it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3));
|
||||
it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6));
|
||||
it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2));
|
||||
it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4));
|
||||
it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9));
|
||||
|
||||
// sicp
|
||||
it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486));
|
||||
it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666));
|
||||
it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495));
|
||||
it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2));
|
||||
it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7));
|
||||
it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75));
|
||||
it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200));
|
||||
it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19));
|
||||
it('sicp 9.4', () =>
|
||||
expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+
|
||||
(* 3
|
||||
(+
|
||||
(* 2 4)
|
||||
(+ 3 5)
|
||||
)
|
||||
)
|
||||
(+
|
||||
(- 10 7) 6
|
||||
)
|
||||
)`)); // this is not exactly pretty printing by convention..
|
||||
|
||||
let scope = {};
|
||||
it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10));
|
||||
it('sicp 11.2', () =>
|
||||
expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159));
|
||||
it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0));
|
||||
it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318));
|
||||
it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390));
|
||||
it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
|
||||
// it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
|
||||
it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441));
|
||||
it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49));
|
||||
it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81));
|
||||
it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0));
|
||||
it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25));
|
||||
it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136));
|
||||
it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136));
|
||||
|
||||
it('sicp 22.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (abs x)
|
||||
(match
|
||||
((gt x 0) x)
|
||||
((eq x 0) 0)
|
||||
((lt x 0) (- 0 x))
|
||||
))`, // sicp was doing (- x), which doesnt work with our -
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false));
|
||||
it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true));
|
||||
it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true));
|
||||
it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false));
|
||||
|
||||
it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3));
|
||||
it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3));
|
||||
it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12));
|
||||
|
||||
it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0));
|
||||
it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13));
|
||||
it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true));
|
||||
it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false));
|
||||
|
||||
it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0));
|
||||
it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0));
|
||||
it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19));
|
||||
it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4));
|
||||
it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16));
|
||||
it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6));
|
||||
it('sicp ex1.1.7', () =>
|
||||
expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16));
|
||||
|
||||
// .. cant use "+" and "-" as standalone expressions, because they are parsed as operators...
|
||||
it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0));
|
||||
it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4));
|
||||
it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4));
|
||||
|
||||
// 1.1.7 Example: Square Roots by Newton’s Method
|
||||
it('sicp 30.1', () =>
|
||||
expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0));
|
||||
it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true));
|
||||
it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false));
|
||||
it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0));
|
||||
it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19));
|
||||
it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0));
|
||||
it('sicp 31.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (sqrtiter guess x) (if (goodenuf guess x)
|
||||
guess
|
||||
(sqrtiter (improve guess x) x)))`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0));
|
||||
it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138));
|
||||
it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145));
|
||||
// eslint-disable-next-line no-loss-of-precision
|
||||
it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925));
|
||||
it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366));
|
||||
|
||||
// lexical scoping
|
||||
it('sicp 39.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sqrt x)
|
||||
(def (goodenough guess)
|
||||
(lt (abs (- (square guess) x)) 0.001))
|
||||
(def (improve guess)
|
||||
(average guess (/ x guess)))
|
||||
(def (sqrt-iter guess)
|
||||
(if (goodenough guess) guess (sqrt-iter (improve guess))))
|
||||
(sqrtiter 1.0))
|
||||
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// recursive fac
|
||||
it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0));
|
||||
it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
|
||||
|
||||
// iterative fac
|
||||
it('sicp 41.3', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (factorial n) (factiter 1 1 n))
|
||||
(def (factiter product counter maxcount)
|
||||
(if (gt counter maxcount)
|
||||
product
|
||||
(factiter (* counter product)
|
||||
(+ counter 1)
|
||||
maxcount)))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
|
||||
|
||||
// 46.1
|
||||
/* (def (+ a b)
|
||||
(if (= a 0) b (inc (+ (dec a) b))))
|
||||
(def (+ a b)
|
||||
(if (= a 0) b (+ (dec a) (inc b)))) */
|
||||
|
||||
// Exercise 1.10
|
||||
// Ackermann’s function
|
||||
it('sicp 47.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (A x y) (match ((eq y 0) 0)
|
||||
((eq x 0) (* 2 y))
|
||||
((eq y 1) 2)
|
||||
(else (A (- x 1) (A x (- y 1))))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024));
|
||||
it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536));
|
||||
it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536));
|
||||
it('sicp 47.5', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f n) (A 0 n))
|
||||
(def (g n) (A 1 n)))
|
||||
(def (h n) (A 2 n))
|
||||
(def (k n) (* 5 n n))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// Tree Recursion
|
||||
// recursive process
|
||||
it('sicp 48.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1)
|
||||
(else (+ (fib (- n 1)) (fib (- n 2))))))
|
||||
(fib 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(13));
|
||||
|
||||
// iterative process
|
||||
it('sicp 48.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fib n) (fibiter 1 0 n))
|
||||
(def (fibiter a b count) (if (eq count 0)
|
||||
b
|
||||
(fibiter (+ a b) a (- count 1))))
|
||||
(fib 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(13));
|
||||
|
||||
// example: counting change
|
||||
it('sicp 52.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (countchange amount) (cc amount 5))
|
||||
(def (cc amount kindsofcoins)
|
||||
(match
|
||||
((eq amount 0) 1)
|
||||
((or (lt amount 0) (eq kindsofcoins 0)) 0)
|
||||
(else (+
|
||||
(cc amount (- kindsofcoins 1))
|
||||
(cc (- amount (firstdenomination kindsofcoins)) kindsofcoins)))))
|
||||
|
||||
(def (firstdenomination kindsofcoins)
|
||||
(match
|
||||
((eq kindsofcoins 1) 1)
|
||||
((eq kindsofcoins 2) 5)
|
||||
((eq kindsofcoins 3) 10)
|
||||
((eq kindsofcoins 4) 25)
|
||||
((eq kindsofcoins 5) 50)))
|
||||
|
||||
(countchange 100)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(292));
|
||||
|
||||
// todo: pascals triangle
|
||||
it('sicp 57.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (cube x) (* x x x))
|
||||
(def (p x) (sub (* 3 x) (* 4 (cube x))))
|
||||
(def (sine angle)
|
||||
(if (not (gt (abs angle) 0.1)) angle
|
||||
(p (sine (/ angle 3.0)))))
|
||||
|
||||
(sine 12.15)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(-0.39980345741334));
|
||||
|
||||
// exponentiation recursive
|
||||
it('sicp 57.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1)))))
|
||||
(expt 2 4)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(16));
|
||||
|
||||
// exponentiation iterative
|
||||
it('sicp 58.1b', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (expt b n) (exptiter b n 1))
|
||||
(def (exptiter b counter product) (if (eq counter 0)
|
||||
product
|
||||
(exptiter b (- counter 1) (* b product))))
|
||||
(expt 2 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(32));
|
||||
|
||||
// exponentiation fast
|
||||
it('sicp 58.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fastexpt b n) (match ((eq n 0) 1)
|
||||
((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1))))))
|
||||
(def (iseven n)
|
||||
(eq (mod n 2) 0))
|
||||
(fastexpt 2 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(32));
|
||||
|
||||
// * = repeated addition
|
||||
it('sicp 60.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (mult a b) (if (eq b 0)
|
||||
0
|
||||
(+ a (* a (- b 1)))))
|
||||
(mult 3 15)
|
||||
`,
|
||||
),
|
||||
).toEqual(45));
|
||||
|
||||
// gcd / euclid
|
||||
it('sicp 63.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (gcd a b) (if (eq b 0)
|
||||
a
|
||||
(gcd b (mod a b))))
|
||||
(gcd 20 6)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2));
|
||||
|
||||
// 65 smallest divisor
|
||||
// 67 fermat test
|
||||
// ....
|
||||
|
||||
// higher order procedures
|
||||
|
||||
it('sicp 77.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sum term a next b)
|
||||
(if (gt a b) 0 (+ (term a)
|
||||
(sum term (next a) next b))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp 78.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (inc n) (+ n 1))
|
||||
(def (cube a) (* a a a))
|
||||
(def (sumcubes a b)
|
||||
(sum cube a inc b))
|
||||
(sumcubes 1 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3025));
|
||||
|
||||
it('sicp 78.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (identity x) x)
|
||||
(def (sumintegers a b)
|
||||
(sum identity a inc b))
|
||||
(sumintegers 1 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(55));
|
||||
|
||||
// pisum
|
||||
it('sicp 79.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (pisum a b)
|
||||
(def (piterm x)
|
||||
(/ 1.0 (* x (+ x 2))))
|
||||
(def (pinext x) (+ x 4))
|
||||
(sum piterm a pinext b))
|
||||
(* 8 (pisum 1 1000))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.139592655589783));
|
||||
|
||||
// integral
|
||||
it('sicp 79.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (integral f a b dx)
|
||||
(def (adddx x) (+ x dx))
|
||||
(* (sum f (+ a (/ dx 2.0)) adddx b) dx))
|
||||
(integral cube 0 1 0.01)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.24998750000000042));
|
||||
// maximum callstack...
|
||||
//it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001));
|
||||
|
||||
//lambdas
|
||||
it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9));
|
||||
|
||||
it('sicp 83.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (pisum a b)
|
||||
(sum (fn (x) (/ 1.0 (* x (+ x 2))))
|
||||
a
|
||||
(fn (x) (+ x 4))
|
||||
b))
|
||||
(* 8 (pisum 1 1000))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.139592655589783));
|
||||
it('sicp 83.3', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (integral f a b dx)
|
||||
(* (sum f
|
||||
(+ a (/ dx 2.0))
|
||||
(fn (x) (+ x dx))
|
||||
b)
|
||||
dx))
|
||||
(integral cube 0 1 0.01)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.24998750000000042));
|
||||
it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12));
|
||||
|
||||
// let expressions
|
||||
it('sicp 87.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(+ (let ((x 3))
|
||||
(+ x (* x 10))) x)
|
||||
`,
|
||||
{ x: 5 },
|
||||
),
|
||||
).toEqual(38));
|
||||
it('sicp 87.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(let ((x 3)
|
||||
(y (+ x 2)))
|
||||
(* x y))
|
||||
`,
|
||||
{ x: 2 },
|
||||
),
|
||||
).toEqual(12));
|
||||
it('sicp 88.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f g) (g 2))
|
||||
(f square)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(4));
|
||||
it('sicp 88.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f g) (g 2))
|
||||
(f (fn (z) (* z (+ z 1))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(6));
|
||||
|
||||
// Finding roots of equations by the half-interval method
|
||||
it('sicp 89.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (search f negpoint pospoint)
|
||||
(let ((midpoint (average negpoint pospoint)))
|
||||
(if (closeenough negpoint pospoint)
|
||||
midpoint
|
||||
(let ((testvalue (f midpoint)))
|
||||
(match ((positive testvalue)
|
||||
(search f negpoint midpoint))
|
||||
((negative testvalue)
|
||||
(search f midpoint pospoint))
|
||||
(else midpoint))))))
|
||||
|
||||
(def (closeenough x y) (lt (abs (- x y)) 0.001))
|
||||
|
||||
(def (negative x) (lt x 0))
|
||||
(def (positive x) (gt x 0))
|
||||
|
||||
(def (halfintervalmethod f a b) (let ((avalue (f a))
|
||||
(bvalue (f b)))
|
||||
(match ((and (negative avalue) (positive bvalue))
|
||||
(search f a b))
|
||||
((and (negative bvalue) (positive avalue))
|
||||
(search f b a)) (else
|
||||
(error "Values are not of opposite sign" a b)))))
|
||||
|
||||
(halfintervalmethod sin 2.0 4.0)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.14111328125));
|
||||
|
||||
it('sicp 89.1', () =>
|
||||
expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625));
|
||||
|
||||
// Finding fixed points of functions
|
||||
it('sicp 92.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def tolerance 0.00001)
|
||||
(def (fixedpoint f first-guess)
|
||||
(def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance))
|
||||
(def (try guess)
|
||||
(let ((next (f guess)))
|
||||
(if (closeenough guess next) next (try next))))
|
||||
(try first-guess))
|
||||
|
||||
(fixedpoint cos 1.0)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.7390822985224023));
|
||||
it('sicp 93.1', () =>
|
||||
expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173));
|
||||
// Maximum call stack size exceeded (expected)
|
||||
/* it('sicp 93.2', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */
|
||||
it('sicp 93.3', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
|
||||
2.6457513110645907,
|
||||
));
|
||||
// Procedures as Returned Values
|
||||
it('sicp 97.1', () =>
|
||||
expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55));
|
||||
it('sicp 98.1', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
|
||||
2.6457513110645907,
|
||||
));
|
||||
it('sicp 98.2', () =>
|
||||
expect(
|
||||
evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope),
|
||||
).toEqual(1.912934258514886));
|
||||
it('sicp 99.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx)))
|
||||
(def dx 0.00001)
|
||||
(def (cube x) (* x x x))
|
||||
((deriv cube) 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(75.00014999664018));
|
||||
// With the aid of deriv, we can express Newton’s method as a fixed-point process:
|
||||
it('sicp 100.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (newtontransform g)
|
||||
(fn (x) (- x (/ (g x) ((deriv g) x)))))
|
||||
(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess))
|
||||
(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
// whatever this is
|
||||
it('sicp 101.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess))
|
||||
(def (sqrt x) (fixedpointoftransform
|
||||
(fn (y) (/ x y)) averagedamp 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
it('sicp 101.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sqrt x) (fixedpointoftransform
|
||||
(fn (y) (- (square y) x)) newtontransform 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
|
||||
// data abstraction
|
||||
|
||||
// rational arithmetic
|
||||
it('sicp 114.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (addrat x y)
|
||||
(makerat (+ (*
|
||||
(numer x) (denom y))
|
||||
(* (numer y) (denom x)))
|
||||
(* (denom x) (denom y))))
|
||||
(def (subrat x y)
|
||||
(makerat (- (* (numer x) (denom y))
|
||||
(* (numer y) (denom x)))
|
||||
(* (denom x) (denom y))))
|
||||
(def (mulrat x y)
|
||||
(makerat (* (numer x) (numer y))
|
||||
(* (denom x) (denom y))))
|
||||
(def (divrat x y)
|
||||
(makerat (* (numer x) (denom y))
|
||||
(* (denom x) (numer y))))
|
||||
(def (equalrat x y)
|
||||
(eq (* (numer x) (denom y))
|
||||
(* (numer y) (denom x))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// markerat number denom
|
||||
it('sicp 117.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (makerat n d) (cons n d))
|
||||
(def (numer x) (car x))
|
||||
(def (denom x) (cdr x))
|
||||
(def (printrat x) (concat (numer x) ':' (denom x)))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2'));
|
||||
it('sicp 117.2', () =>
|
||||
expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6'));
|
||||
it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6'));
|
||||
it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9'));
|
||||
it('sicp 118.1', () =>
|
||||
expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0));
|
||||
it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3'));
|
||||
|
||||
let lscope = {};
|
||||
// pairs with lambda
|
||||
it('sicp 124.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (cons x y)
|
||||
(def (dispatch m)
|
||||
(match
|
||||
((eq m 0) x)
|
||||
((eq m 1) y)
|
||||
(else (error "argument not 0 or 1: CONS" m)))
|
||||
) dispatch)
|
||||
(def (car z) (z 0))
|
||||
(def (cdr z) (z 1))
|
||||
`,
|
||||
lscope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first'));
|
||||
it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second'));
|
||||
// lists
|
||||
it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4]));
|
||||
it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1));
|
||||
it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4]));
|
||||
it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2));
|
||||
it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4]));
|
||||
// listref
|
||||
it('sicp 138.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (listref items n) (if (eq n 0) (car items)
|
||||
(listref (cdr items) (- n 1))))
|
||||
(def squares (list 1 4 9 16 25))
|
||||
(listref squares 3)`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(16));
|
||||
// length recursive
|
||||
it('sicp 138.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (length items)
|
||||
(if (isnull items) 0
|
||||
(+ 1 (length (cdr items)))))
|
||||
(def odds (list 1 3 5 7))
|
||||
(length odds)`,
|
||||
),
|
||||
).toEqual(4));
|
||||
// length iterative
|
||||
it('sicp 139.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (length items)
|
||||
(def (lengthiter a count)
|
||||
(if (isnull a) count
|
||||
(lengthiter (cdr a) (+ 1 count))))
|
||||
(lengthiter items 0))
|
||||
(def odds (list 1 3 5 7))
|
||||
(length odds)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(4));
|
||||
// append
|
||||
it('sicp 139.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (append list1 list2)
|
||||
(if (isnull list1)
|
||||
list2
|
||||
(cons (car list1) (append (cdr list1) list2))))
|
||||
(append squares odds)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7]));
|
||||
// (define (f x y . z) ⟨body⟩) <- tbd: variable argument count
|
||||
// Mapping over lists
|
||||
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (scalelist items factor) (if (isnull items) nil
|
||||
(cons (* (car items) factor)
|
||||
(scalelist (cdr items) factor))))
|
||||
(scalelist (list 1 2 3 4 5) 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 20, 30, 40, 50]));
|
||||
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (map proc items) (if (isnull items) nil
|
||||
(cons (proc (car items))
|
||||
(map proc (cdr items)))))
|
||||
(map abs (list -10 2.5 -11.6 17))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 2.5, 11.6, 17]));
|
||||
it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16]));
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (scalelist items factor) (map (fn (x) (* x factor)) items))
|
||||
(scalelist (list 1 2 3 4 5) 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 20, 30, 40, 50]));
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
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, 'mondo.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'mondo.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
// external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/mondough
|
||||
|
||||
connects mondo to strudel.
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
strudelScope,
|
||||
reify,
|
||||
fast,
|
||||
slow,
|
||||
seq,
|
||||
stepcat,
|
||||
extend,
|
||||
expand,
|
||||
pace,
|
||||
chooseIn,
|
||||
degradeBy,
|
||||
silence,
|
||||
} from '@strudel/core';
|
||||
import { registerLanguage } from '@strudel/transpiler';
|
||||
import { MondoRunner } from 'mondolang';
|
||||
|
||||
const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
|
||||
|
||||
const arrayRange = (start, stop, step = 1) =>
|
||||
Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) =>
|
||||
start < stop ? start + index * step : start - index * step,
|
||||
);
|
||||
const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b))));
|
||||
|
||||
let nope = (...args) => args[args.length - 1];
|
||||
|
||||
let lib = {};
|
||||
lib['nope'] = nope;
|
||||
lib['-'] = (a, b) => b.early(a);
|
||||
lib['+'] = (a, b) => b.late(a);
|
||||
lib['_'] = silence;
|
||||
lib['~'] = silence;
|
||||
lib.curly = stepcat;
|
||||
lib.square = (...args) => stepcat(...args).setSteps(1);
|
||||
lib.angle = (...args) => stepcat(...args).pace(1);
|
||||
lib['*'] = fast;
|
||||
lib['/'] = slow;
|
||||
lib['!'] = extend;
|
||||
lib['@'] = expand;
|
||||
lib['%'] = pace;
|
||||
lib['?'] = degradeBy; // todo: default 0.5 not working..
|
||||
lib[':'] = tail;
|
||||
lib['..'] = range;
|
||||
lib['def'] = () => silence;
|
||||
lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]"
|
||||
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
|
||||
|
||||
function evaluator(node, scope) {
|
||||
const { type } = node;
|
||||
// node is list
|
||||
if (type === 'list') {
|
||||
const { children } = node;
|
||||
const [name, ...args] = children;
|
||||
// some functions wont be reified to make sure they work (e.g. see extend below)
|
||||
if (typeof name === 'function') {
|
||||
return name(...args);
|
||||
}
|
||||
if (name.value === 'def') {
|
||||
return silence;
|
||||
}
|
||||
// name is expected to be a pattern of functions!
|
||||
const first = name.firstCycle(true)[0];
|
||||
const type = typeof first?.value;
|
||||
if (type !== 'function') {
|
||||
throw new Error(`[mondough] expected function, got "${first?.value}"`);
|
||||
}
|
||||
return name
|
||||
.fmap((fn) => {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`[mondough] "${fn}" is not a function b`);
|
||||
}
|
||||
return fn(...args);
|
||||
})
|
||||
.innerJoin();
|
||||
}
|
||||
// node is leaf
|
||||
let { value } = node;
|
||||
if (type === 'plain' && scope[value]) {
|
||||
return reify(scope[value]); // -> local scope has no location
|
||||
}
|
||||
const variable = lib[value] ?? strudelScope[value];
|
||||
// problem: collisions when we want a string that happens to also be a variable name
|
||||
// example: "s sine" -> sine is also a variable
|
||||
let pat;
|
||||
if (type === 'plain' && typeof variable !== 'undefined') {
|
||||
// some function names are not patternable, so we skip reification here
|
||||
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
|
||||
return variable;
|
||||
}
|
||||
pat = reify(variable);
|
||||
} else {
|
||||
pat = reify(value);
|
||||
}
|
||||
if (node.loc) {
|
||||
pat = pat.withLoc(node.loc[0], node.loc[1]);
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
|
||||
let runner = new MondoRunner({ evaluator });
|
||||
|
||||
export function mondo(code, offset = 0) {
|
||||
if (Array.isArray(code)) {
|
||||
code = code.join('');
|
||||
}
|
||||
const pat = runner.run(code, undefined, offset);
|
||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||
}
|
||||
|
||||
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||
|
||||
export const mondi = (str, offset) => {
|
||||
const code = `[${str}]`;
|
||||
return mondo(code, offset);
|
||||
};
|
||||
|
||||
// tell transpiler how to get locations for mondo`` calls
|
||||
registerLanguage('mondo', {
|
||||
getLocations,
|
||||
});
|
||||
|
||||
// this is like mondo, but with a zero offset
|
||||
export const mondolang = (code) => mondo(code, 0);
|
||||
registerLanguage('mondolang', {
|
||||
getLocations: (code) => getLocations(code, 0),
|
||||
});
|
||||
// uncomment the following to use mondo as mini notation language
|
||||
/* registerLanguage('minilang', {
|
||||
name: 'mondi',
|
||||
getLocations,
|
||||
}); */
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@strudel/mondo",
|
||||
"version": "1.1.1",
|
||||
"description": "mondo notation for strudel",
|
||||
"main": "mondough.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/mondough.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"bench": "vitest bench",
|
||||
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs",
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"mondolang": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mondo": "*",
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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, 'mondough.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
// external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "MQTT API for strudel",
|
||||
"main": "mqtt.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -60,7 +60,7 @@ export function parseControlsFromHap(hap, cps) {
|
||||
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||
const osc = await connect();
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/osc",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/reference",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"description": "Headless reference of all strudel functions",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Strudel REPL as a Web Component",
|
||||
"module": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function prebake() {
|
||||
samples(`${ds}/tidal-drum-machines.json`),
|
||||
samples(`${ds}/piano.json`),
|
||||
samples(`${ds}/Dirt-Samples.json`),
|
||||
samples(`${ds}/EmuSP12.json`),
|
||||
samples(`${ds}/uzu-drumkit.json`),
|
||||
samples(`${ds}/vcsl.json`),
|
||||
samples(`${ds}/mridangam.json`),
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/serial",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -537,7 +537,7 @@ export default {
|
||||
],
|
||||
gm_synth_bass_1: [
|
||||
// Synth Bass 1: Bass
|
||||
'0380_Aspirin_sf2_file',
|
||||
// '0380_Aspirin_sf2_file', // broken in safari https://codeberg.org/uzu/strudel/issues/1384
|
||||
'0380_Chaos_sf2_file',
|
||||
'0380_FluidR3_GM_sf2_file',
|
||||
// 0380_GeneralUserGS_sf2_file // laut
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/soundfonts",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
|
||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||
|
||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
return this.onTrigger((time_deprecate, h, ct, cps, targetTime) => {
|
||||
return this.onTrigger((h, ct, cps, targetTime) => {
|
||||
const ctx = getAudioContext();
|
||||
const note = getPlayableNoteValue(h);
|
||||
const preset = sf.presets[n % sf.presets.length];
|
||||
|
||||
@@ -74,6 +74,6 @@ export const dough = async (code) => {
|
||||
worklet.node.connect(ac.destination);
|
||||
};
|
||||
|
||||
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
|
||||
export function doughTrigger(hap, currentTime, cps, targetTime) {
|
||||
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { clamp, nanFallback } from './util.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
|
||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
|
||||
export function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
@@ -206,7 +209,8 @@ export function getVibratoOscillator(param, value, t) {
|
||||
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
||||
// a bit of a hack, but it works very well :)
|
||||
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||
const constantNode = audioContext.createConstantSource();
|
||||
const constantNode = new ConstantSourceNode(audioContext);
|
||||
|
||||
constantNode.start(startTime);
|
||||
constantNode.stop(stopTime);
|
||||
constantNode.onended = () => {
|
||||
@@ -216,9 +220,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||
}
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
const ctx = getAudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
let osc;
|
||||
if (noises.includes(type)) {
|
||||
osc = ctx.createBufferSource();
|
||||
osc.buffer = getNoiseBuffer(type, 2);
|
||||
osc.loop = true;
|
||||
} else {
|
||||
osc = ctx.createOscillator();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
}
|
||||
|
||||
osc.start();
|
||||
const g = new GainNode(ctx, { gain: range });
|
||||
osc.connect(g); // -range, range
|
||||
@@ -253,7 +265,7 @@ export function applyFM(param, value, begin) {
|
||||
|
||||
modulator = fmmod.node;
|
||||
stop = fmmod.stop;
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
|
||||
// no envelope by default
|
||||
modulator.connect(param);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
let log = (msg) => console.log(msg);
|
||||
|
||||
export function errorLogger(e, origin = 'cyclist') {
|
||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
||||
// console.error(e);
|
||||
logger(`[${origin}] error: ${e.message}`);
|
||||
}
|
||||
|
||||
export const logger = (...args) => log(...args);
|
||||
|
||||
export const setLogger = (fn) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
|
||||
let noiseCache = {};
|
||||
|
||||
// lazy generates noise buffers and keeps them forever
|
||||
function getNoiseBuffer(type, density) {
|
||||
export function getNoiseBuffer(type, density) {
|
||||
const ac = getAudioContext();
|
||||
if (noiseCache[type]) {
|
||||
return noiseCache[type];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -7,11 +7,11 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { clamp, nanFallback, _mod } from './util.mjs';
|
||||
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
|
||||
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { logger, errorLogger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
|
||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||
@@ -28,6 +28,13 @@ export function setMultiChannelOrbits(bool) {
|
||||
multiChannelOrbits = bool == true;
|
||||
}
|
||||
|
||||
function getModulationShapeInput(val) {
|
||||
if (typeof val === 'number') {
|
||||
return val % 5;
|
||||
}
|
||||
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||
}
|
||||
|
||||
export const soundMap = map();
|
||||
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
@@ -107,6 +114,10 @@ export async function aliasBank(...args) {
|
||||
}
|
||||
|
||||
export function getSound(s) {
|
||||
if (typeof s !== 'string') {
|
||||
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
||||
return soundMap.get().triangle; // is this good?
|
||||
}
|
||||
return soundMap.get()[s.toLowerCase()];
|
||||
}
|
||||
|
||||
@@ -122,7 +133,7 @@ export const getAudioDevices = async () => {
|
||||
return devicesMap;
|
||||
};
|
||||
|
||||
const defaultDefaultValues = {
|
||||
let defaultDefaultValues = {
|
||||
s: 'triangle',
|
||||
gain: 0.8,
|
||||
postgain: 1,
|
||||
@@ -139,13 +150,24 @@ const defaultDefaultValues = {
|
||||
delay: 0,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
delaytime: 0.25,
|
||||
delaysync: 3 / 16,
|
||||
orbit: 1,
|
||||
i: 1,
|
||||
velocity: 1,
|
||||
fft: 8,
|
||||
};
|
||||
|
||||
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
|
||||
|
||||
export function setDefault(control, value) {
|
||||
// const main = getControlName(control); // we cant do this because superdough is independent of strudel/core
|
||||
defaultDefaultValues[control] = value;
|
||||
}
|
||||
|
||||
export function resetDefaults() {
|
||||
defaultDefaultValues = { ...defaultDefaultDefaultValues };
|
||||
}
|
||||
|
||||
let defaultControls = new Map(Object.entries(defaultDefaultValues));
|
||||
|
||||
export function setDefaultValue(key, value) {
|
||||
@@ -309,30 +331,40 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
|
||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||
}
|
||||
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
||||
if (!delays[orbit]) {
|
||||
if (!orbits[orbit].delayNode) {
|
||||
const ac = getAudioContext();
|
||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||
connectToDestination(dly, channels);
|
||||
delays[orbit] = dly;
|
||||
connectToOrbit(dly, orbit);
|
||||
orbits[orbit].delayNode = dly;
|
||||
}
|
||||
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
|
||||
return delays[orbit];
|
||||
orbits[orbit].delayNode.delayTime.value !== delaytime &&
|
||||
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
|
||||
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
|
||||
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
|
||||
return orbits[orbit].delayNode;
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, time, end, properties = {}) {
|
||||
return getWorklet(audioContext, 'lfo-processor', {
|
||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
const { shape = 0, ...props } = properties;
|
||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||
const lfoprops = {
|
||||
frequency: 1,
|
||||
depth: 1,
|
||||
skew: 0,
|
||||
depth,
|
||||
skew: 0.5,
|
||||
phaseoffset: 0,
|
||||
time,
|
||||
time: begin,
|
||||
begin,
|
||||
end,
|
||||
shape: 1,
|
||||
dcoffset: -0.5,
|
||||
...properties,
|
||||
});
|
||||
shape: getModulationShapeInput(shape),
|
||||
dcoffset,
|
||||
min: dcoffset * depth,
|
||||
max: dcoffset * depth + depth,
|
||||
curve: 1,
|
||||
...props,
|
||||
};
|
||||
|
||||
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||
}
|
||||
|
||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
@@ -366,31 +398,73 @@ function getFilterType(ftype) {
|
||||
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
|
||||
}
|
||||
|
||||
let reverbs = {};
|
||||
//type orbit {
|
||||
// gain: number,
|
||||
// reverbNode: reverbNode
|
||||
// delayNode: delayNode
|
||||
//}
|
||||
let orbits = {};
|
||||
function connectToOrbit(node, orbit) {
|
||||
if (orbits[orbit] == null) {
|
||||
errorLogger(new Error('target orbit does not exist'), 'superdough');
|
||||
}
|
||||
node.connect(orbits[orbit].gain);
|
||||
}
|
||||
|
||||
function setOrbit(audioContext, orbit, channels) {
|
||||
if (orbits[orbit] == null) {
|
||||
orbits[orbit] = {
|
||||
gain: new GainNode(audioContext, { gain: 1 }),
|
||||
};
|
||||
connectToDestination(orbits[orbit].gain, channels);
|
||||
}
|
||||
}
|
||||
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
|
||||
const targetArr = [targetOrbit].flat();
|
||||
|
||||
targetArr.forEach((target) => {
|
||||
if (orbits[target] == null) {
|
||||
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
|
||||
return;
|
||||
}
|
||||
webAudioTimeout(
|
||||
audioContext,
|
||||
() => {
|
||||
orbits[target].gain.gain.cancelScheduledValues(t);
|
||||
const currVal = orbits[target].gain.gain.value;
|
||||
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
|
||||
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
|
||||
},
|
||||
0,
|
||||
t - 0.01,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
|
||||
function getReverb(orbit, duration, fade, lp, dim, ir) {
|
||||
// If no reverb has been created for a given orbit, create one
|
||||
if (!reverbs[orbit]) {
|
||||
if (!orbits[orbit].reverbNode) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
|
||||
connectToDestination(reverb, channels);
|
||||
reverbs[orbit] = reverb;
|
||||
connectToOrbit(reverb, orbit);
|
||||
orbits[orbit].reverbNode = reverb;
|
||||
}
|
||||
if (
|
||||
hasChanged(duration, reverbs[orbit].duration) ||
|
||||
hasChanged(fade, reverbs[orbit].fade) ||
|
||||
hasChanged(lp, reverbs[orbit].lp) ||
|
||||
hasChanged(dim, reverbs[orbit].dim) ||
|
||||
reverbs[orbit].ir !== ir
|
||||
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
|
||||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
|
||||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
|
||||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
|
||||
orbits[orbit].reverbNode.ir !== ir
|
||||
) {
|
||||
// only regenerate when something has changed
|
||||
// avoids endless regeneration on things like
|
||||
// stack(s("a"), s("b").rsize(8)).room(.5)
|
||||
// this only works when args may stay undefined until here
|
||||
// setting default values breaks this
|
||||
reverbs[orbit].generate(duration, fade, lp, dim, ir);
|
||||
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
|
||||
}
|
||||
return reverbs[orbit];
|
||||
return orbits[orbit].reverbNode;
|
||||
}
|
||||
|
||||
export let analysers = {},
|
||||
@@ -433,8 +507,7 @@ function effectSend(input, effect, wet) {
|
||||
}
|
||||
|
||||
export function resetGlobalEffects() {
|
||||
delays = {};
|
||||
reverbs = {};
|
||||
orbits = {};
|
||||
analysers = {};
|
||||
analysersData = {};
|
||||
}
|
||||
@@ -446,9 +519,10 @@ function mapChannelNumbers(channels) {
|
||||
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||
}
|
||||
|
||||
export const superdough = async (value, t, hapDuration, cps) => {
|
||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
||||
// new: t is always expected to be the absolute target onset time
|
||||
const ac = getAudioContext();
|
||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||
|
||||
let { stretch } = value;
|
||||
if (stretch != null) {
|
||||
//account for phase vocoder latency
|
||||
@@ -474,15 +548,25 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
}
|
||||
// destructure
|
||||
let {
|
||||
tremolo,
|
||||
tremolosync,
|
||||
tremolodepth = 1,
|
||||
tremoloskew,
|
||||
tremolophase = 0,
|
||||
tremoloshape,
|
||||
s = getDefaultValue('s'),
|
||||
bank,
|
||||
source,
|
||||
gain = getDefaultValue('gain'),
|
||||
postgain = getDefaultValue('postgain'),
|
||||
density = getDefaultValue('density'),
|
||||
duckorbit,
|
||||
duckattack,
|
||||
duckdepth,
|
||||
// filters
|
||||
fanchor = getDefaultValue('fanchor'),
|
||||
drive = 0.69,
|
||||
release = 0,
|
||||
// low pass
|
||||
cutoff,
|
||||
lpenv,
|
||||
@@ -515,7 +599,9 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
phasercenter,
|
||||
//
|
||||
coarse,
|
||||
|
||||
crush,
|
||||
dry,
|
||||
shape,
|
||||
shapevol = getDefaultValue('shapevol'),
|
||||
distort,
|
||||
@@ -524,7 +610,8 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
vowel,
|
||||
delay = getDefaultValue('delay'),
|
||||
delayfeedback = getDefaultValue('delayfeedback'),
|
||||
delaytime = getDefaultValue('delaytime'),
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
delaytime,
|
||||
orbit = getDefaultValue('orbit'),
|
||||
room,
|
||||
roomfade,
|
||||
@@ -543,10 +630,18 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
compressorRelease,
|
||||
} = value;
|
||||
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
|
||||
const orbitChannels = mapChannelNumbers(
|
||||
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
|
||||
);
|
||||
|
||||
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
|
||||
setOrbit(ac, orbit, channels, t, cycle, cps);
|
||||
|
||||
if (duckorbit != null) {
|
||||
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
|
||||
}
|
||||
|
||||
gain = applyGainCurve(nanFallback(gain, 1));
|
||||
postgain = applyGainCurve(postgain);
|
||||
@@ -554,8 +649,11 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
distortvol = applyGainCurve(distortvol);
|
||||
delay = applyGainCurve(delay);
|
||||
velocity = applyGainCurve(velocity);
|
||||
tremolodepth = applyGainCurve(tremolodepth);
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
|
||||
const end = t + hapDuration;
|
||||
const endWithRelease = end + release;
|
||||
const chainID = Math.round(Math.random() * 1000000);
|
||||
|
||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||
@@ -571,6 +669,9 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
|
||||
let audioNodes = [];
|
||||
|
||||
if (['-', '~', '_'].includes(s)) {
|
||||
return;
|
||||
}
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
value.s = s;
|
||||
@@ -627,7 +728,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
lprelease,
|
||||
lpenv,
|
||||
t,
|
||||
t + hapDuration,
|
||||
end,
|
||||
fanchor,
|
||||
ftype,
|
||||
drive,
|
||||
@@ -651,7 +752,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
hprelease,
|
||||
hpenv,
|
||||
t,
|
||||
t + hapDuration,
|
||||
end,
|
||||
fanchor,
|
||||
);
|
||||
chain.push(hp());
|
||||
@@ -662,20 +763,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
|
||||
if (bandf !== undefined) {
|
||||
let bp = () =>
|
||||
createFilter(
|
||||
ac,
|
||||
'bandpass',
|
||||
bandf,
|
||||
bandq,
|
||||
bpattack,
|
||||
bpdecay,
|
||||
bpsustain,
|
||||
bprelease,
|
||||
bpenv,
|
||||
t,
|
||||
t + hapDuration,
|
||||
fanchor,
|
||||
);
|
||||
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
|
||||
chain.push(bp());
|
||||
if (ftype === '24db') {
|
||||
chain.push(bp());
|
||||
@@ -693,6 +781,33 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
||||
|
||||
if (tremolosync != null) {
|
||||
tremolo = cps * tremolosync;
|
||||
}
|
||||
|
||||
if (tremolo !== undefined) {
|
||||
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
||||
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
||||
const gain = Math.max(1 - tremolodepth, 0);
|
||||
const amGain = new GainNode(ac, { gain });
|
||||
|
||||
const time = cycle / cps;
|
||||
const lfo = getLfo(ac, t, endWithRelease, {
|
||||
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
|
||||
frequency: tremolo,
|
||||
depth: tremolodepth,
|
||||
time,
|
||||
dcoffset: 0,
|
||||
shape: tremoloshape,
|
||||
phaseoffset: tremolophase,
|
||||
min: 0,
|
||||
max: 1,
|
||||
curve: 1.5,
|
||||
});
|
||||
lfo.connect(amGain.gain);
|
||||
chain.push(amGain);
|
||||
}
|
||||
|
||||
compressorThreshold !== undefined &&
|
||||
chain.push(
|
||||
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
||||
@@ -706,20 +821,19 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
}
|
||||
// phaser
|
||||
if (phaser !== undefined && phaserdepth > 0) {
|
||||
const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep);
|
||||
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
|
||||
chain.push(phaserFX);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = new GainNode(ac, { gain: postgain });
|
||||
chain.push(post);
|
||||
connectToDestination(post, channels);
|
||||
|
||||
// delay
|
||||
let delaySend;
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
|
||||
delaySend = effectSend(post, delyNode, delay);
|
||||
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
|
||||
delaySend = effectSend(post, delayNode, delay);
|
||||
audioNodes.push(delaySend);
|
||||
}
|
||||
// reverb
|
||||
@@ -748,6 +862,14 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
analyserSend = effectSend(post, analyserNode, 1);
|
||||
audioNodes.push(analyserSend);
|
||||
}
|
||||
if (dry != null) {
|
||||
dry = applyGainCurve(dry);
|
||||
const dryGain = new GainNode(ac, { gain: dry });
|
||||
chain.push(dryGain);
|
||||
connectToOrbit(dryGain, orbit);
|
||||
} else {
|
||||
connectToOrbit(post, orbit);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext, getLfo } from './superdough.mjs';
|
||||
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
gainNode,
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
getVibratoOscillator,
|
||||
webAudioTimeout,
|
||||
getWorklet,
|
||||
noises,
|
||||
} from './helpers.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
|
||||
const getFrequencyFromValue = (value) => {
|
||||
const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||
let { note, freq } = value;
|
||||
note = note || 36;
|
||||
note = note || defaultNote;
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note); // e.g. c3 => 48
|
||||
}
|
||||
@@ -34,7 +35,23 @@ function destroyAudioWorkletNode(node) {
|
||||
}
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||
const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
const waveformAliases = [
|
||||
['tri', 'triangle'],
|
||||
['sqr', 'square'],
|
||||
['saw', 'sawtooth'],
|
||||
['sin', 'sine'],
|
||||
];
|
||||
|
||||
function makeSaturationCurve(amount, n_samples) {
|
||||
const k = typeof amount === 'number' ? amount : 50;
|
||||
const curve = new Float32Array(n_samples);
|
||||
|
||||
for (let i = 0; i < n_samples; i++) {
|
||||
const x = (i * 2) / n_samples - 1;
|
||||
curve[i] = Math.tanh(x * k);
|
||||
}
|
||||
return curve;
|
||||
}
|
||||
|
||||
export function registerSynthSounds() {
|
||||
[...waveforms].forEach((s) => {
|
||||
@@ -78,6 +95,75 @@ export function registerSynthSounds() {
|
||||
{ type: 'synth', prebake: true },
|
||||
);
|
||||
});
|
||||
|
||||
registerSound(
|
||||
'sbd',
|
||||
(t, value, onended) => {
|
||||
const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value;
|
||||
const ctx = getAudioContext();
|
||||
const attackhold = 0.02;
|
||||
const noiselvl = 1.2;
|
||||
const noisedecay = 0.025;
|
||||
const mixGain = 1;
|
||||
|
||||
const o = ctx.createOscillator();
|
||||
o.type = 'triangle';
|
||||
o.frequency.value = getFrequencyFromValue(value, 29);
|
||||
o.detune.setValueAtTime(penv * 100, 0);
|
||||
o.detune.setValueAtTime(penv * 100, t);
|
||||
o.detune.exponentialRampToValueAtTime(0.001, t + pdecay);
|
||||
const g = gainNode(1);
|
||||
g.gain.setValueAtTime(1, t + attackhold);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay);
|
||||
o.start(t);
|
||||
|
||||
const noise = getNoiseOscillator('brown', t, 2);
|
||||
const noiseGain = gainNode(1);
|
||||
noiseGain.gain.setValueAtTime(noiselvl, t);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay);
|
||||
|
||||
const sat = new WaveShaperNode(ctx);
|
||||
// tri to sine diode shaper emulation
|
||||
sat.curve = makeSaturationCurve(2, ctx.sampleRate);
|
||||
|
||||
const mix = gainNode(mixGain);
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
sat.disconnect();
|
||||
noise.node.disconnect();
|
||||
noiseGain.disconnect();
|
||||
mix.disconnect();
|
||||
onended();
|
||||
};
|
||||
|
||||
const node = o.connect(sat).connect(g).connect(mix);
|
||||
noise.node.connect(noiseGain).connect(mix);
|
||||
|
||||
const holdEnd = t + decay;
|
||||
let end = holdEnd + 0.01;
|
||||
if (clip != null) {
|
||||
end = Math.min(t + clip * duration, end);
|
||||
}
|
||||
|
||||
// prevent clicking
|
||||
mix.gain.setValueAtTime(mixGain, end - 0.01);
|
||||
mix.gain.linearRampToValueAtTime(0, end);
|
||||
|
||||
o.stop(end);
|
||||
noise.stop(end);
|
||||
|
||||
return {
|
||||
node,
|
||||
stop: (endTime) => {
|
||||
o.stop(endTime);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
);
|
||||
|
||||
registerSound(
|
||||
'supersaw',
|
||||
(begin, value, onended) => {
|
||||
@@ -342,6 +428,7 @@ export function registerSynthSounds() {
|
||||
{ type: 'synth', prebake: true },
|
||||
);
|
||||
});
|
||||
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
|
||||
}
|
||||
|
||||
export function waveformN(partials, type) {
|
||||
|
||||
@@ -68,3 +68,11 @@ export const _mod = (n, m) => ((n % m) + m) % m;
|
||||
export const getSoundIndex = (n, numSounds) => {
|
||||
return _mod(Math.round(nanFallback(n, 0)), numSounds);
|
||||
};
|
||||
|
||||
export function cycleToSeconds(cycle, cps) {
|
||||
return cycle / cps;
|
||||
}
|
||||
|
||||
export function secondsToCycle(t, cps) {
|
||||
return t * cps;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,28 @@ import FFT from './fft.js';
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
const _mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
// Restrict phase to the range [0, maxPhase) via wrapping
|
||||
function wrapPhase(phase, maxPhase = 1) {
|
||||
if (phase >= maxPhase) {
|
||||
phase -= maxPhase;
|
||||
} else if (phase < 0) {
|
||||
phase += maxPhase;
|
||||
}
|
||||
return phase;
|
||||
}
|
||||
const blockSize = 128;
|
||||
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
|
||||
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
|
||||
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
||||
function polyBlep(phase, dt) {
|
||||
// 0 <= phase < 1
|
||||
dt = Math.min(dt, 1 - dt);
|
||||
// Start of cycle
|
||||
if (phase < dt) {
|
||||
phase /= dt;
|
||||
// 2 * (phase - phase^2/2 - 0.5)
|
||||
return phase + phase - phase * phase - 1;
|
||||
}
|
||||
|
||||
// -1 < phase < 0
|
||||
// End of cycle
|
||||
else if (phase > 1 - dt) {
|
||||
phase = (phase - 1) / dt;
|
||||
// 2 * (phase^2/2 + phase + 0.5)
|
||||
@@ -31,7 +41,7 @@ function polyBlep(phase, dt) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The order is important for dough integration
|
||||
const waveshapes = {
|
||||
tri(phase, skew = 0.5) {
|
||||
const x = 1 - skew;
|
||||
@@ -81,10 +91,12 @@ function getParamValue(block, param) {
|
||||
}
|
||||
return param[0];
|
||||
}
|
||||
|
||||
const waveShapeNames = Object.keys(waveshapes);
|
||||
class LFOProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0 },
|
||||
{ name: 'time', defaultValue: 0 },
|
||||
{ name: 'end', defaultValue: 0 },
|
||||
{ name: 'frequency', defaultValue: 0.5 },
|
||||
@@ -92,7 +104,10 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'depth', defaultValue: 1 },
|
||||
{ name: 'phaseoffset', defaultValue: 0 },
|
||||
{ name: 'shape', defaultValue: 0 },
|
||||
{ name: 'curve', defaultValue: 1 },
|
||||
{ name: 'dcoffset', defaultValue: 0 },
|
||||
{ name: 'min', defaultValue: 0 },
|
||||
{ name: 'max', defaultValue: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -109,10 +124,13 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const begin = parameters['begin'][0];
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= begin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const output = outputs[0];
|
||||
const frequency = parameters['frequency'][0];
|
||||
@@ -122,7 +140,11 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
const skew = parameters['skew'][0];
|
||||
const phaseoffset = parameters['phaseoffset'][0];
|
||||
|
||||
const curve = parameters['curve'][0];
|
||||
|
||||
const dcoffset = parameters['dcoffset'][0];
|
||||
const min = parameters['min'][0];
|
||||
const max = parameters['max'][0];
|
||||
const shape = waveShapeNames[parameters['shape'][0]];
|
||||
|
||||
const blockSize = output[0].length ?? 0;
|
||||
@@ -130,12 +152,12 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
if (this.phase == null) {
|
||||
this.phase = _mod(time * frequency + phaseoffset, 1);
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const dt = frequency / sampleRate;
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
||||
output[i][n] = modval;
|
||||
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
||||
modval = Math.pow(modval, curve);
|
||||
output[i][n] = clamp(modval, min, max);
|
||||
}
|
||||
this.incrementPhase(dt);
|
||||
}
|
||||
@@ -291,7 +313,6 @@ class LadderProcessor extends AudioWorkletProcessor {
|
||||
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
||||
|
||||
let cutoff = parameters.frequency[0];
|
||||
// eslint-disable-next-line no-undef
|
||||
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
||||
cutoff = cutoff > 1 ? 1 : cutoff;
|
||||
|
||||
@@ -424,18 +445,13 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
process(input, outputs, params) {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
if (currentTime >= params.end[0]) {
|
||||
// this.port.postMessage({ type: 'onended' });
|
||||
return false;
|
||||
}
|
||||
let frequency = params.frequency[0];
|
||||
//apply detune in cents
|
||||
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
|
||||
|
||||
const output = outputs[0];
|
||||
const voices = params.voices[0];
|
||||
@@ -446,9 +462,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const isOdd = (n & 1) == 1;
|
||||
|
||||
//applies unison "spread" detune in semitones
|
||||
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
@@ -456,21 +469,21 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const dt = freq / sampleRate;
|
||||
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
// Main detuning
|
||||
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
|
||||
// Individual voice detuning
|
||||
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
// has domain [0, 1]
|
||||
const dt = _mod(freq / sampleRate, 1);
|
||||
this.phase[n] = this.phase[n] ?? Math.random();
|
||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||
|
||||
output[0][i] = output[0][i] + v * gainL;
|
||||
output[1][i] = output[1][i] + v * gainR;
|
||||
|
||||
this.phase[n] += dt;
|
||||
|
||||
if (this.phase[n] > 1.0) {
|
||||
this.phase[n] = this.phase[n] - 1;
|
||||
}
|
||||
this.phase[n] = wrapPhase(this.phase[n] + dt);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -479,7 +492,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
||||
|
||||
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||
const BUFFERED_BLOCK_SIZE = 2048;
|
||||
|
||||
function genHannWindow(length) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/tonal",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -25,28 +25,35 @@ describe('tonal', () => {
|
||||
});
|
||||
it('scale with n values', () => {
|
||||
expect(
|
||||
n(0, 1, 2)
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale with colon', () => {
|
||||
expect(
|
||||
n(0, 1, 2)
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C:major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale without tonic', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('scale with mininotation colon', () => {
|
||||
expect(
|
||||
n(0, 1, 2)
|
||||
n(seq(0, 1, 2))
|
||||
.scale(mini('C:major'))
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('transposes note numbers with interval numbers', () => {
|
||||
expect(
|
||||
note(40, 40, 40)
|
||||
note(seq(40, 40, 40))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 41, 42]);
|
||||
@@ -54,7 +61,7 @@ describe('tonal', () => {
|
||||
});
|
||||
it('transposes note numbers with interval strings', () => {
|
||||
expect(
|
||||
note(40, 40, 40)
|
||||
note(seq(40, 40, 40))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 42, 43]);
|
||||
@@ -62,7 +69,7 @@ describe('tonal', () => {
|
||||
});
|
||||
it('transposes note strings with interval numbers', () => {
|
||||
expect(
|
||||
note('c', 'c', 'c')
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'Db', 'D']);
|
||||
@@ -70,7 +77,7 @@ describe('tonal', () => {
|
||||
});
|
||||
it('transposes note strings with interval strings', () => {
|
||||
expect(
|
||||
note('c', 'c', 'c')
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'D', 'Eb']);
|
||||
|
||||
+23
-18
@@ -14,7 +14,7 @@ function scaleStep(step, scale) {
|
||||
scale = scale.replaceAll(':', ' ');
|
||||
step = Math.ceil(step);
|
||||
let { intervals, tonic, empty } = Scale.get(scale);
|
||||
if ((empty && isNote(scale)) || (!empty && !tonic)) {
|
||||
if ((empty && isNote(scale)) || (empty && !tonic)) {
|
||||
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
|
||||
} else if (empty) {
|
||||
throw new Error(`invalid scale "${scale}"`);
|
||||
@@ -88,13 +88,14 @@ function scaleOffset(scale, offset, note) {
|
||||
* @returns Pattern
|
||||
* @memberof Pattern
|
||||
* @name transpose
|
||||
* @synonyms trans
|
||||
* @example
|
||||
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
|
||||
* @example
|
||||
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
|
||||
*/
|
||||
|
||||
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
|
||||
export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
const note = hap.value.note ?? hap.value;
|
||||
if (typeof note === 'number') {
|
||||
@@ -142,6 +143,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
|
||||
* @name scaleTranspose
|
||||
* @param {offset} offset number of steps inside the scale
|
||||
* @returns Pattern
|
||||
* @synonyms scaleTrans, strans
|
||||
* @example
|
||||
* "-8 [2,4,6]"
|
||||
* .scale('C4 bebop major')
|
||||
@@ -149,22 +151,25 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
|
||||
* .note()
|
||||
*/
|
||||
|
||||
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
if (!hap.context.scale) {
|
||||
throw new Error('can only use scaleTranspose after .scale');
|
||||
}
|
||||
if (typeof hap.value === 'object')
|
||||
return hap.withValue(() => ({
|
||||
...hap.value,
|
||||
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
|
||||
}));
|
||||
if (typeof hap.value !== 'string') {
|
||||
throw new Error('can only use scaleTranspose with notes');
|
||||
}
|
||||
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
||||
});
|
||||
});
|
||||
export const { scaleTranspose, scaleTrans, strans } = register(
|
||||
['scaleTranspose', 'scaleTrans', 'strans'],
|
||||
function (offset /* : number | string */, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
if (!hap.context.scale) {
|
||||
throw new Error('can only use scaleTranspose after .scale');
|
||||
}
|
||||
if (typeof hap.value === 'object')
|
||||
return hap.withValue(() => ({
|
||||
...hap.value,
|
||||
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
|
||||
}));
|
||||
if (typeof hap.value !== 'string') {
|
||||
throw new Error('can only use scaleTranspose with notes');
|
||||
}
|
||||
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/transpiler",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -8,6 +8,16 @@ export function registerWidgetType(type) {
|
||||
widgetMethods.push(type);
|
||||
}
|
||||
|
||||
let languages = new Map();
|
||||
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
||||
// see mondough.mjs for example use
|
||||
// the language will kick in when the code contains a template literal of type
|
||||
// example: mondo`...` will use language of type "mondo"
|
||||
// TODO: refactor tidal.mjs to use this
|
||||
export function registerLanguage(type, config) {
|
||||
languages.set(type, config);
|
||||
}
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
|
||||
@@ -19,14 +29,33 @@ export function transpiler(input, options = {}) {
|
||||
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
const minilang = languages.get('minilang');
|
||||
if (minilang) {
|
||||
const code = `[${value}]`;
|
||||
const locs = minilang.getLocations(code, node.start);
|
||||
miniLocations = miniLocations.concat(locs);
|
||||
} else {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
}
|
||||
};
|
||||
let widgets = [];
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent /* , prop, index */) {
|
||||
if (isTidalTeplateLiteral(node)) {
|
||||
if (isLanguageLiteral(node)) {
|
||||
const { name } = node.tag;
|
||||
const language = languages.get(name);
|
||||
const code = node.quasi.quasis[0].value.raw;
|
||||
const offset = node.quasi.start + 1;
|
||||
if (emitMiniLocations) {
|
||||
const locs = language.getLocations(code, offset);
|
||||
miniLocations = miniLocations.concat(locs);
|
||||
}
|
||||
this.skip();
|
||||
return this.replace(languageWithLocation(name, code, offset));
|
||||
}
|
||||
if (isTemplateLiteral(node, 'tidal')) {
|
||||
const raw = node.quasi.quasis[0].value.raw;
|
||||
const offset = node.quasi.start + 1;
|
||||
if (emitMiniLocations) {
|
||||
@@ -84,8 +113,18 @@ export function transpiler(input, options = {}) {
|
||||
leave(node, parent, prop, index) {},
|
||||
});
|
||||
|
||||
const { body } = ast;
|
||||
if (!body?.[body.length - 1]?.expression) {
|
||||
let { body } = ast;
|
||||
|
||||
if (!body.length) {
|
||||
console.warn('empty body -> fallback to silence');
|
||||
body.push({
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'Identifier',
|
||||
name: 'silence',
|
||||
},
|
||||
});
|
||||
} else if (!body?.[body.length - 1]?.expression) {
|
||||
throw new Error('unexpected ast format without body expression');
|
||||
}
|
||||
|
||||
@@ -120,11 +159,18 @@ function isBackTickString(node, parent) {
|
||||
|
||||
function miniWithLocation(value, node) {
|
||||
const { start: fromOffset } = node;
|
||||
|
||||
const minilang = languages.get('minilang');
|
||||
let name = 'm';
|
||||
if (minilang && minilang.name) {
|
||||
name = minilang.name; // name is expected to be exported from the package of the minilang
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: 'm',
|
||||
name,
|
||||
},
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
@@ -219,12 +265,16 @@ function labelToP(node) {
|
||||
};
|
||||
}
|
||||
|
||||
function isLanguageLiteral(node) {
|
||||
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
|
||||
}
|
||||
|
||||
// tidal highlighting
|
||||
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
|
||||
// but maybe it's the only way
|
||||
|
||||
function isTidalTeplateLiteral(node) {
|
||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal';
|
||||
function isTemplateLiteral(node, value) {
|
||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
||||
}
|
||||
|
||||
function collectHaskellMiniLocations(haskellCode, offset) {
|
||||
@@ -262,3 +312,18 @@ function tidalWithLocation(value, offset) {
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function languageWithLocation(name, value, offset) {
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
{ type: 'Literal', value: offset },
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/web",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||
"module": "web.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/webaudio",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -15,13 +15,10 @@ const hap2value = (hap) => {
|
||||
return hap.value;
|
||||
};
|
||||
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
|
||||
// uses more precise, absolute t if available, see https://codeberg.org/uzu/strudel/pulls/1004
|
||||
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) =>
|
||||
superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration);
|
||||
|
||||
Pattern.prototype.webaudio = function () {
|
||||
return this.onTrigger(webaudioOutputTrigger);
|
||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||
// TODO: refactor output callbacks to eliminate deadline
|
||||
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||
};
|
||||
|
||||
export function webaudioRepl(options = {}) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/xen",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
Generated
+118
@@ -347,6 +347,37 @@ importers:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/mondo:
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vitest:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/mondough:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@strudel/transpiler':
|
||||
specifier: workspace:*
|
||||
version: link:../transpiler
|
||||
mondolang:
|
||||
specifier: workspace:*
|
||||
version: link:../mondo
|
||||
devDependencies:
|
||||
mondo:
|
||||
specifier: '*'
|
||||
version: 0.4.4
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vitest:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/motion:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@@ -683,6 +714,9 @@ importers:
|
||||
'@strudel/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/mini
|
||||
'@strudel/mondo':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/mondough
|
||||
'@strudel/motion':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/motion
|
||||
@@ -2975,6 +3009,10 @@ packages:
|
||||
resolution: {integrity: sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==}
|
||||
engines: {node: '>= 14.0.0'}
|
||||
|
||||
amdefine@1.0.1:
|
||||
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
|
||||
engines: {node: '>=0.4.2'}
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
@@ -3103,6 +3141,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
async@0.2.10:
|
||||
resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
@@ -3337,6 +3378,9 @@ packages:
|
||||
claviature@0.1.0:
|
||||
resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==}
|
||||
|
||||
cldr-plurals@1.0.0:
|
||||
resolution: {integrity: sha512-xGkehDsjj/ng1LtYiAzdiqDgqTQ/qmWafDlB6R8CfXbpXe4Ge2Tl4l7gxiA+yaD1WCTP3EVfwerdmVwfco7vxw==}
|
||||
|
||||
clean-stack@2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4378,6 +4422,9 @@ packages:
|
||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
globalize@0.1.1:
|
||||
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
||||
|
||||
globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4439,6 +4486,11 @@ packages:
|
||||
h3@1.14.0:
|
||||
resolution: {integrity: sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==}
|
||||
|
||||
handlebars@1.1.2:
|
||||
resolution: {integrity: sha512-6lekK3aSHE7XnEqEs+JWQJRSRdCqJYHEVjEfWWZv9pjLUYZNeP26EtlgOrpDCSX+k5N1m74urTbztgBOCko1Kg==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
|
||||
handlebars@4.7.8:
|
||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
@@ -5586,6 +5638,10 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
mondo@0.4.4:
|
||||
resolution: {integrity: sha512-3ot6iKwC9KZvwmyZ9dgPCnlt0O1GuBnK8QZyhnoJdtkIiRL9X/cmQuu88CbFSznQx6bq19xQdwJHpv8hggH62Q==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
mrmime@2.0.0:
|
||||
resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5827,6 +5883,9 @@ packages:
|
||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
optimist@0.3.7:
|
||||
resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -6725,6 +6784,10 @@ packages:
|
||||
source-map-support@0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
|
||||
source-map@0.1.43:
|
||||
resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
source-map@0.5.7:
|
||||
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7155,6 +7218,11 @@ packages:
|
||||
ufo@1.5.4:
|
||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||
|
||||
uglify-js@2.3.6:
|
||||
resolution: {integrity: sha512-T2LWWydxf5+Btpb0S/Gg/yKFmYjnX9jtQ4mdN9YRq73BhN21EhU0Dvw3wYDLqd3TooGUJlCKf3Gfyjjy/RTcWA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@@ -7173,6 +7241,9 @@ packages:
|
||||
underscore@1.13.7:
|
||||
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
||||
|
||||
underscore@1.5.2:
|
||||
resolution: {integrity: sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==}
|
||||
|
||||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
@@ -7555,6 +7626,10 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
wordwrap@0.0.3:
|
||||
resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
wordwrap@1.0.0:
|
||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||
|
||||
@@ -10438,6 +10513,9 @@ snapshots:
|
||||
'@algolia/requester-fetch': 5.20.0
|
||||
'@algolia/requester-node-http': 5.20.0
|
||||
|
||||
amdefine@1.0.1:
|
||||
optional: true
|
||||
|
||||
ansi-align@3.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -10647,6 +10725,9 @@ snapshots:
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
async@0.2.10:
|
||||
optional: true
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
@@ -10904,6 +10985,8 @@ snapshots:
|
||||
|
||||
claviature@0.1.0: {}
|
||||
|
||||
cldr-plurals@1.0.0: {}
|
||||
|
||||
clean-stack@2.2.0: {}
|
||||
|
||||
cli-boxes@3.0.0: {}
|
||||
@@ -12046,6 +12129,8 @@ snapshots:
|
||||
minipass: 4.2.8
|
||||
path-scurry: 1.11.1
|
||||
|
||||
globalize@0.1.1: {}
|
||||
|
||||
globals@11.12.0: {}
|
||||
|
||||
globals@14.0.0: {}
|
||||
@@ -12112,6 +12197,12 @@ snapshots:
|
||||
uncrypto: 0.1.3
|
||||
unenv: 1.10.0
|
||||
|
||||
handlebars@1.1.2:
|
||||
dependencies:
|
||||
optimist: 0.3.7
|
||||
optionalDependencies:
|
||||
uglify-js: 2.3.6
|
||||
|
||||
handlebars@4.7.8:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
@@ -13695,6 +13786,13 @@ snapshots:
|
||||
requirejs: 2.3.7
|
||||
requirejs-config-file: 4.0.0
|
||||
|
||||
mondo@0.4.4:
|
||||
dependencies:
|
||||
cldr-plurals: 1.0.0
|
||||
globalize: 0.1.1
|
||||
handlebars: 1.1.2
|
||||
underscore: 1.5.2
|
||||
|
||||
mrmime@2.0.0: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
@@ -13987,6 +14085,10 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
optimist@0.3.7:
|
||||
dependencies:
|
||||
wordwrap: 0.0.3
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -15094,6 +15196,11 @@ snapshots:
|
||||
buffer-from: 1.1.2
|
||||
source-map: 0.6.1
|
||||
|
||||
source-map@0.1.43:
|
||||
dependencies:
|
||||
amdefine: 1.0.1
|
||||
optional: true
|
||||
|
||||
source-map@0.5.7: {}
|
||||
|
||||
source-map@0.6.1: {}
|
||||
@@ -15561,6 +15668,13 @@ snapshots:
|
||||
|
||||
ufo@1.5.4: {}
|
||||
|
||||
uglify-js@2.3.6:
|
||||
dependencies:
|
||||
async: 0.2.10
|
||||
optimist: 0.3.7
|
||||
source-map: 0.1.43
|
||||
optional: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
optional: true
|
||||
|
||||
@@ -15577,6 +15691,8 @@ snapshots:
|
||||
|
||||
underscore@1.13.7: {}
|
||||
|
||||
underscore@1.5.2: {}
|
||||
|
||||
undici-types@6.20.0: {}
|
||||
|
||||
unenv@1.10.0:
|
||||
@@ -15957,6 +16073,8 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wordwrap@0.0.3: {}
|
||||
|
||||
wordwrap@1.0.0: {}
|
||||
|
||||
workbox-background-sync@7.0.0:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,7 @@ const toneHelpersMocked = {
|
||||
'_pianoroll',
|
||||
'_spectrum',
|
||||
'markcss',
|
||||
'p',
|
||||
].forEach((mock) => {
|
||||
strudel.Pattern.prototype[mock] = function () {
|
||||
return this;
|
||||
@@ -163,6 +164,7 @@ evalScope(
|
||||
loadCsound,
|
||||
loadcsound,
|
||||
setcps: id,
|
||||
setcpm: id,
|
||||
Clock: {}, // whatever
|
||||
},
|
||||
);
|
||||
|
||||
@@ -359,28 +359,6 @@ stack(
|
||||
"[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25)
|
||||
).note().piano().slow(2)`;
|
||||
|
||||
/*
|
||||
export const customTrigger = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
// by Felix Roos
|
||||
stack(
|
||||
freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))),
|
||||
freq("440(5,8)".clip(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8))
|
||||
).s("<sawtooth square>/2")
|
||||
.onTrigger((t,hap,ct)=>{
|
||||
const ac = Tone.getContext().rawContext;
|
||||
t = ac.currentTime + t - ct;
|
||||
const { freq, s, gain = 1 } = hap.value;
|
||||
const master = ac.createGain();
|
||||
master.gain.value = 0.1 * gain;
|
||||
master.connect(ac.destination);
|
||||
const o = ac.createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
o.frequency.value = Number(freq);
|
||||
o.connect(master);
|
||||
o.start(t);
|
||||
o.stop(t + hap.duration);
|
||||
}).stack(s("bd(3,8),hh*4,~ sd").webdirt())`; */
|
||||
|
||||
export const swimmingWithSoundfonts = `// Koji Kondo - Swimming (Super Mario World)
|
||||
stack(
|
||||
n(
|
||||
|
||||
@@ -647,7 +647,6 @@
|
||||
"evaluate"
|
||||
],
|
||||
"/packages/webaudio/webaudio.mjs": [
|
||||
"webaudioOutputTrigger",
|
||||
"webaudioOutput",
|
||||
"webaudioRepl"
|
||||
],
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"@strudel/mondo": "workspace:*",
|
||||
"@strudel/xen": "workspace:*",
|
||||
"@supabase/supabase-js": "^2.48.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"_base": "https://raw.githubusercontent.com/ritchse/tidal-drum-machines/main/machines/EmuSP12/",
|
||||
"bd": ["emusp12-bd/Bassdrum-01.wav","emusp12-bd/Bassdrum-02.wav","emusp12-bd/Bassdrum-03.wav","emusp12-bd/Bassdrum-04.wav","emusp12-bd/Bassdrum-05.wav","emusp12-bd/Bassdrum-06.wav","emusp12-bd/Bassdrum-07.wav","emusp12-bd/Bassdrum-08.wav","emusp12-bd/Bassdrum-09.wav","emusp12-bd/Bassdrum-10.wav","emusp12-bd/Bassdrum-11.wav","emusp12-bd/Bassdrum-12.wav","emusp12-bd/Bassdrum-13.wav","emusp12-bd/Bassdrum-14.wav"],
|
||||
"cb": ["emusp12-cb/Cowbell.wav"],
|
||||
"cp": ["emusp12-cp/Clap.wav"],
|
||||
"cr": ["emusp12-cr/Crash.wav"],
|
||||
"hh": ["emusp12-hh/Hat Closed-01.wav","emusp12-hh/Hat Closed-02.wav"],
|
||||
"ht": ["emusp12-ht/Tom H-01.wav","emusp12-ht/Tom H-02.wav","emusp12-ht/Tom H-03.wav","emusp12-ht/Tom H-04.wav","emusp12-ht/Tom H-05.wav","emusp12-ht/Tom H-06.wav"],
|
||||
"lt": ["emusp12-lt/Tom L-01.wav","emusp12-lt/Tom L-02.wav","emusp12-lt/Tom L-03.wav","emusp12-lt/Tom L-04.wav","emusp12-lt/Tom L-05.wav","emusp12-lt/Tom L-06.wav"],
|
||||
"misc": ["emusp12-misc/Metal-01.wav","emusp12-misc/Metal-02.wav","emusp12-misc/Metal-03.wav","emusp12-misc/Scratch.wav","emusp12-misc/Shot-01.wav","emusp12-misc/Shot-02.wav","emusp12-misc/Shot-03.wav"],
|
||||
"mt": ["emusp12-mt/Tom M-01.wav","emusp12-mt/Tom M-02.wav","emusp12-mt/Tom M-03.wav","emusp12-mt/Tom M-05.wav"],
|
||||
"oh": ["emusp12-oh/Hhopen1.wav"],
|
||||
"perc": ["emusp12-perc/Blow1.wav"],
|
||||
"rd": ["emusp12-rd/Ride.wav"],
|
||||
"rim": ["emusp12-rim/zRim Shot-01.wav","emusp12-rim/zRim Shot-02.wav"],
|
||||
"sd": ["emusp12-sd/Snaredrum-01.wav","emusp12-sd/Snaredrum-02.wav","emusp12-sd/Snaredrum-03.wav","emusp12-sd/Snaredrum-04.wav","emusp12-sd/Snaredrum-05.wav","emusp12-sd/Snaredrum-06.wav","emusp12-sd/Snaredrum-07.wav","emusp12-sd/Snaredrum-08.wav","emusp12-sd/Snaredrum-09.wav","emusp12-sd/Snaredrum-10.wav","emusp12-sd/Snaredrum-11.wav","emusp12-sd/Snaredrum-12.wav","emusp12-sd/Snaredrum-13.wav","emusp12-sd/Snaredrum-14.wav","emusp12-sd/Snaredrum-15.wav","emusp12-sd/Snaredrum-16.wav","emusp12-sd/Snaredrum-17.wav","emusp12-sd/Snaredrum-18.wav","emusp12-sd/Snaredrum-19.wav","emusp12-sd/Snaredrum-20.wav","emusp12-sd/Snaredrum-21.wav"]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/",
|
||||
"bd": [
|
||||
"bd/10_bd_switchangel.wav",
|
||||
"bd/11_bd_mot4i.wav",
|
||||
"bd/12_bd_mot4i.wav",
|
||||
"bd/13_bd_mot4i.wav",
|
||||
"bd/14_bd_switchangel.wav",
|
||||
"bd/15_bd_switchangel.wav",
|
||||
"bd/16_bd_switchangel.wav",
|
||||
"bd/17_bd_switchangel.wav"
|
||||
],
|
||||
"brk": [
|
||||
"brk/10_break_amen_pprocessed.wav"
|
||||
],
|
||||
"cb": [
|
||||
"cb/10_perc_switchangel.wav"
|
||||
],
|
||||
"cp": [
|
||||
"cp/10_cp_switchangel.wav",
|
||||
"cp/11_cp_mot4i.wav"
|
||||
],
|
||||
"cr": [
|
||||
"cr/10_cr_switchangel.wav",
|
||||
"cr/11_cr_mot4i.wav"
|
||||
],
|
||||
"hh": [
|
||||
"hh/10_hh_switchangel.wav",
|
||||
"hh/11_hh_mot4i.wav",
|
||||
"hh/12_hh_switchangel.wav",
|
||||
"hh/13_hh_switchangel.wav",
|
||||
"hh/14_hh_mot4i.wav"
|
||||
],
|
||||
"ht": [
|
||||
"ht/10_ht_mot4i.wav"
|
||||
],
|
||||
"lt": [
|
||||
"lt/10_lt_mot4i.wav"
|
||||
],
|
||||
"misc": [
|
||||
"misc/10_misc_switchangel_ludens.wav",
|
||||
"misc/11_misc_switchangel_ludens.wav",
|
||||
"misc/12_misc_switchangel_ludens.wav",
|
||||
"misc/13_misc_switchangel_ludens.wav",
|
||||
"misc/14_misc_switchangel_ludens.wav"
|
||||
],
|
||||
"mt": [
|
||||
"mt/10_mt_mot4i.wav"
|
||||
],
|
||||
"oh": [
|
||||
"oh/10_oh_switchangel.wav",
|
||||
"oh/11_oh_switchangel.wav",
|
||||
"oh/12_oh_switchangel.wav",
|
||||
"oh/13_oh_switchangel.wav"
|
||||
],
|
||||
"rd": [
|
||||
"rd/10_rd_switchangel.wav"
|
||||
],
|
||||
"rim": [
|
||||
"rim/10_rim_switchangel.wav",
|
||||
"rim/11_rim_switch_angel.wav"
|
||||
],
|
||||
"sd": [
|
||||
"sd/10_sd_switchangel-bounce-2.wav",
|
||||
"sd/11_sd_switchangel_3.wav",
|
||||
"sd/12_sd_switchangel_2.wav",
|
||||
"sd/13_sd_switchangel_2.wav",
|
||||
"sd/14_sd.wav"
|
||||
],
|
||||
"sh": [
|
||||
"sh/10_sh_switchangel.wav"
|
||||
],
|
||||
"tb": [
|
||||
"tb/10_tb.wav"
|
||||
]
|
||||
}
|
||||
@@ -81,6 +81,7 @@ export const SIDEBAR: Sidebar = {
|
||||
{ text: 'Visual Feedback', link: 'learn/visual-feedback' },
|
||||
{ text: 'Offline', link: 'learn/pwa' },
|
||||
{ text: 'Patterns', link: 'technical-manual/patterns' },
|
||||
{ text: 'Mondo Notation', link: 'learn/mondo-notation' },
|
||||
{ text: 'Music metadata', link: 'learn/metadata' },
|
||||
{ text: 'CSound', link: 'learn/csound' },
|
||||
{ text: 'Hydra', link: 'learn/hydra' },
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
title: 'Release Notes v1.1.0'
|
||||
description: ''
|
||||
date: '2024-06-02'
|
||||
tags: ['meta']
|
||||
author: froos
|
||||
---
|
||||
|
||||
These are the release notes for Strudel 1.1.0 aka "Bananensplit".
|
||||
|
||||
The last release was over 19 weeks ago, so a lot of things have happened!
|
||||
|
||||
First, here's a little demo, teasing some of the new features:
|
||||
|
||||
import { Youtube } from '@src/components/Youtube';
|
||||
|
||||
<Youtube client:only="react" id="2Qu5lMHruio" />
|
||||
|
||||
Let's write up some of the highlights:
|
||||
|
||||
## New DSP Features
|
||||
|
||||
### Stereo Supersaw
|
||||
|
||||
with spread, unison, and detune parameters
|
||||
|
||||
```plaintext
|
||||
note("d f a a# a d3").fast(2)
|
||||
.s("supersaw").spread(".8").detune(.3).unison("2 7")
|
||||
```
|
||||
|
||||
### Analog "ladder" filter type
|
||||
|
||||
works great for acid basslines and vibey tones
|
||||
|
||||
```plaintext
|
||||
note("{d d d a a# d3 f4}%16".sub(12)).gain(1).s("sawtooth")
|
||||
.lpf(200).lpenv(slider(1.36,0,8)).lpq(7).distort("1.5:.7")`
|
||||
.ftype('ladder')
|
||||
```
|
||||
|
||||
### stereo distortion effect
|
||||
|
||||
```plaintext
|
||||
note("{g g a# g g4}%8".add("{0 7 12 0}%8")).lpf(500)
|
||||
.s("supersaw").dist("4:.2")
|
||||
```
|
||||
|
||||
## Editor Features
|
||||
|
||||
### inline viz
|
||||
|
||||
The editor now supports multiple visuals within the code, using the `_` prefix for viz functions:
|
||||
|
||||
<img
|
||||
width="595"
|
||||
alt="Screenshot 2024-06-01 at 01 23 51"
|
||||
src="https://codeberg.org/uzu/strudel/assets/12023032/978fee85-e533-4da6-a245-d20d0083f57e"
|
||||
/>
|
||||
|
||||
- `._pianoroll()`: inline pianoroll
|
||||
- `._punchcard()`: inline punchcard
|
||||
- `._scope()`: inline scope
|
||||
- `._pitchwheel()`: inline pitchwheel
|
||||
|
||||
For more info, check out the new [Visual Feedback Page](https://strudel.cc/learn/visual-feedback/)
|
||||
|
||||
### label notation
|
||||
|
||||
This new notation simplifies writing patterns at the top level:
|
||||
|
||||
```plaintext
|
||||
d1: s("bd*4")
|
||||
d2: s("[- hh]*4")
|
||||
```
|
||||
|
||||
This is equivalent to:
|
||||
|
||||
```plaintext
|
||||
stack(
|
||||
s("bd*4"),
|
||||
s("[- hh]*4")
|
||||
)
|
||||
```
|
||||
|
||||
The labels you choose are arbitrary, the above `d1` and `d2` are a typical thing you'd write in tidal, for example `d1 $ s "bd*4"`.
|
||||
If the same label is used multiple times, the last one wins:
|
||||
|
||||
```plaintext
|
||||
d1: s("bd*4")
|
||||
d1: s("[- hh]*4") // <-- only this plays
|
||||
```
|
||||
|
||||
There is a special label anonymous label `$`, which can appear multiple times without overriding itself:
|
||||
|
||||
```plaintext
|
||||
// both of these will play:
|
||||
$: s("bd*4")
|
||||
$: s("[- hh]*4")
|
||||
```
|
||||
|
||||
You can mute a pattern by prefixing `_`:
|
||||
|
||||
```plaintext
|
||||
_$: s("bd*4") // <-- this one is muted
|
||||
$: s("[- hh]*4")
|
||||
```
|
||||
|
||||
To run a transformation on all patterns, you can use `all`:
|
||||
|
||||
```plaintext
|
||||
$: s("bd*4")
|
||||
$: s("[- hh]*4")
|
||||
all(x=>x.room(.5))
|
||||
```
|
||||
|
||||
This notation is now the recommended way to [play patterns in parallel](https://strudel.cc/workshop/first-notes/#playing-multiple-patterns)
|
||||
|
||||
### Clock sync between multiple instances
|
||||
|
||||
timing has received a major overhaul, and is now much more accurate on all browsers. Additionally, you can now sync timing across multiple windows.
|
||||

|
||||
|
||||
### Better sample upload support
|
||||
|
||||
you can now upload large amounts of samples much faster across all browsers including on IOS devices. supported filetypes now include: ogg flac mp3 wav aac m4a
|
||||
|
||||
### experimental tidal syntax
|
||||
|
||||
The new `tidal` function allows you to write strudel patterns in tidal syntax:
|
||||
|
||||
```plaintext
|
||||
await initTidal()
|
||||
|
||||
tidal`
|
||||
d1 $ s "bd*4"
|
||||
|
||||
d2 $ s "[- hh]*4"
|
||||
`
|
||||
```
|
||||
|
||||
As we're looking to improve compatibility with tidal, we're happy to hear feedback.
|
||||
|
||||
## breaking changes
|
||||
|
||||
This release comes with a bunch of breaking changes. If you find your patterns to sound different, check out the PRs below for guidance on how to update them. Most of these changes shouldn't affect a lot of patterns.
|
||||
In case of doubt, add the line `// @version 1.0` to your old pattern.
|
||||
If you're having problems, please let us know!
|
||||
|
||||
- remove legacy legato + duration implementations by @felixroos in https://codeberg.org/uzu/strudel/pull/965
|
||||
- Velocity in value by @felixroos in https://codeberg.org/uzu/strudel/pull/974
|
||||
- use ireal as default voicing dict by @felixroos https://codeberg.org/uzu/strudel/pull/967
|
||||
- Color in hap value by @felixroos https://codeberg.org/uzu/strudel/pull/1007
|
||||
- rename trig -> reset, trigzero -> restart by @felixroos https://codeberg.org/uzu/strudel/pull/1010
|
||||
- remove dangerous arithmetic feature by @felixroos https://codeberg.org/uzu/strudel/pull/1030
|
||||
- change fanchor to 0 by @daslyfe https://codeberg.org/uzu/strudel/pull/1107
|
||||
|
||||
## superdough features
|
||||
|
||||
- replace shape with distort in learn doc by @daslyfe https://codeberg.org/uzu/strudel/pull/982
|
||||
- Worklet Improvents / fixes by @daslyfe https://codeberg.org/uzu/strudel/pull/963
|
||||
- supersaw oscillator by @daslyfe https://codeberg.org/uzu/strudel/pull/978
|
||||
- Add analog-style ladder filter by @daslyfe https://codeberg.org/uzu/strudel/pull/1103
|
||||
- Calculate phaser modulation phase based on time by @daslyfe https://codeberg.org/uzu/strudel/pull/1110
|
||||
- rollback phaser by @daslyfe https://codeberg.org/uzu/strudel/pull/1113
|
||||
|
||||
## editor / ui features
|
||||
|
||||
- 'Enable Bracket Matching' option in Codemirror by @eefano https://codeberg.org/uzu/strudel/pull/956
|
||||
- REPL sync between windows by @daslyfe https://codeberg.org/uzu/strudel/pull/900
|
||||
- inline viz / widgets package by @felixroos https://codeberg.org/uzu/strudel/pull/989
|
||||
- Inline punchcard + spiral by @felixroos https://codeberg.org/uzu/strudel/pull/1008
|
||||
- More fonts by @felixroos https://codeberg.org/uzu/strudel/pull/1023
|
||||
- better theme integration for visuals + various fixes by @felixroos https://codeberg.org/uzu/strudel/pull/1024
|
||||
- add setting for sync flag by @felixroos https://codeberg.org/uzu/strudel/pull/1025
|
||||
- add closeBrackets setting by @felixroos https://codeberg.org/uzu/strudel/pull/1031
|
||||
- add font file types to offline cache by @felixroos https://codeberg.org/uzu/strudel/pull/1032
|
||||
- pitchwheel visual by @felixroos https://codeberg.org/uzu/strudel/pull/1041
|
||||
- repl: set document.title from @title by @kasparsj https://codeberg.org/uzu/strudel/pull/1090
|
||||
- Samples tab improvements by @daslyfe https://codeberg.org/uzu/strudel/pull/1102
|
||||
|
||||
## language features
|
||||
|
||||
- pickOut(), pickRestart(), pickReset() by @eefano https://codeberg.org/uzu/strudel/pull/950
|
||||
- Auto await samples by @felixroos https://codeberg.org/uzu/strudel/pull/955
|
||||
- feat: can now invert euclid pulses with negative numbers by @felixroos https://codeberg.org/uzu/strudel/pull/959
|
||||
- Nested controls by @felixroos https://codeberg.org/uzu/strudel/pull/973
|
||||
- alias - for ~ by @yaxu https://codeberg.org/uzu/strudel/pull/981
|
||||
- Beat-oriented functionality by @yaxu https://codeberg.org/uzu/strudel/pull/976
|
||||
- Labeled statements by @felixroos https://codeberg.org/uzu/strudel/pull/991
|
||||
- accidentals in scale degrees by @eefano https://codeberg.org/uzu/strudel/pull/1000
|
||||
- Feature: tactus marking by @yaxu https://codeberg.org/uzu/strudel/pull/1021
|
||||
- Tactus tidy by @yaxu https://codeberg.org/uzu/strudel/pull/1027
|
||||
- Wax, wane, taper and taperlist by @yaxu https://codeberg.org/uzu/strudel/pull/1042
|
||||
- transpose: support all combinations of numbers and strings for notes and intervals by @felixroos https://codeberg.org/uzu/strudel/pull/1048
|
||||
- anonymous patterns + muting by @felixroos https://codeberg.org/uzu/strudel/pull/1059
|
||||
- add swing + swingBy by @felixroos https://codeberg.org/uzu/strudel/pull/1038
|
||||
- Stepwise functions from Tidal by @yaxu https://codeberg.org/uzu/strudel/pull/1060
|
||||
- Tactus tweaks - fixes for maintaining tactus and highlight locations by @yaxu https://codeberg.org/uzu/strudel/pull/1065
|
||||
- Fix stepjoin by @yaxu https://codeberg.org/uzu/strudel/pull/1067
|
||||
- More tactus tidying by @yaxu https://codeberg.org/uzu/strudel/pull/1071
|
||||
- Tactus calculation toggle and breaking change to tactus calculation in fast/slow/hurry by @yaxu https://codeberg.org/uzu/strudel/pull/1081
|
||||
- hs2js package / tidal parser by @felixroos https://codeberg.org/uzu/strudel/pull/870
|
||||
- Add the mousex and mousey signal by @Enelg52 https://codeberg.org/uzu/strudel/pull/1112
|
||||
- can now access strudelMirror from repl by @felixroos https://codeberg.org/uzu/strudel/pull/1117
|
||||
|
||||
## sampler
|
||||
|
||||
If you have nodejs installed on your system, you can now use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to serve samples from disk to the REPL or flok.
|
||||
|
||||
- local sample server cli by @felixroos https://codeberg.org/uzu/strudel/pull/1033
|
||||
- Fix sampler paths by @felixroos https://codeberg.org/uzu/strudel/pull/1034
|
||||
- Fix sampler windows by @felixroos https://codeberg.org/uzu/strudel/pull/1108
|
||||
- fix sampler on windows by @geikha https://codeberg.org/uzu/strudel/pull/1109
|
||||
|
||||
## docs
|
||||
|
||||
- V1 release notes by @felixroos https://codeberg.org/uzu/strudel/pull/935
|
||||
- Minor documentation error: Update first-sounds.mdx by @mhetrick https://codeberg.org/uzu/strudel/pull/941
|
||||
- Update synths.mdx by @andresgottlieb https://codeberg.org/uzu/strudel/pull/984
|
||||
- using strudel in your project guide + cleanup examples by @felixroos https://codeberg.org/uzu/strudel/pull/1006
|
||||
- Document signals by @ilesinge https://codeberg.org/uzu/strudel/pull/1015
|
||||
- improve tutorial + custom samples doc by @felixroos https://codeberg.org/uzu/strudel/pull/1053
|
||||
- fix cr typo on first-sounds.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1068
|
||||
- fix first sounds typo by @cleary https://codeberg.org/uzu/strudel/pull/1069
|
||||
- add `<...>` to first-sounds.mdx recap by @cleary https://codeberg.org/uzu/strudel/pull/1070
|
||||
- add nesting to `off` example variation in pattern-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1075
|
||||
- fix translation issue in first-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1072
|
||||
- add signals to recap in first-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1073
|
||||
- fix docs on alignment.mdx by @diegodorado https://codeberg.org/uzu/strudel/pull/1076
|
||||
- fix little dub tune example by @lukad https://codeberg.org/uzu/strudel/pull/1104
|
||||
- clarify `off` in pattern-effects.mdx by @cleary https://codeberg.org/uzu/strudel/pull/1074
|
||||
- Fixes drawPianoroll import in codemirror example by @giohappy https://codeberg.org/uzu/strudel/pull/1116
|
||||
- Migrate tutorial fanchor by @felixroos https://codeberg.org/uzu/strudel/pull/1122
|
||||
|
||||
## internals
|
||||
|
||||
- remove cjs builds by @felixroos https://codeberg.org/uzu/strudel/pull/945
|
||||
- controls refactoring: simplify exports by @felixroos https://codeberg.org/uzu/strudel/pull/962
|
||||
- move canvas related helpers from core to new draw package by @felixroos https://codeberg.org/uzu/strudel/pull/971
|
||||
- remove canvas, externalize samples, delete junk by @felixroos https://codeberg.org/uzu/strudel/pull/1003
|
||||
- Improve performance of ! (replicate) by @yaxu https://codeberg.org/uzu/strudel/pull/1084
|
||||
- Benchmarks by @yaxu https://codeberg.org/uzu/strudel/pull/1079
|
||||
|
||||
## fixes
|
||||
|
||||
- fix midi issue on firefox and added quote error by @Enelg52 https://codeberg.org/uzu/strudel/pull/936
|
||||
- fix: pianoroll sorting by @felixroos https://codeberg.org/uzu/strudel/pull/938
|
||||
- account for cps in midi time duration by @daslyfe https://codeberg.org/uzu/strudel/pull/954
|
||||
- fix script importable packages (web + repl) by @felixroos https://codeberg.org/uzu/strudel/pull/957
|
||||
- fix: reset global fx on pattern change by @felixroos https://codeberg.org/uzu/strudel/pull/960
|
||||
- add debounce to logger by @felixroos https://codeberg.org/uzu/strudel/pull/968
|
||||
- fix for transpose(): preserve hap value object structure by @eefano https://codeberg.org/uzu/strudel/pull/966
|
||||
- fix: clear hydra on reset by @felixroos https://codeberg.org/uzu/strudel/pull/983
|
||||
- little fix for withVal by @eefano https://codeberg.org/uzu/strudel/pull/980
|
||||
- fix: share now shares what's visible instead of active by @felixroos https://codeberg.org/uzu/strudel/pull/985
|
||||
- Fix pure mini highlight by @yaxu https://codeberg.org/uzu/strudel/pull/994
|
||||
- fix: await injectPatternMethods by @felixroos https://codeberg.org/uzu/strudel/pull/1012
|
||||
- update undocumented script by @felixroos https://codeberg.org/uzu/strudel/pull/1013
|
||||
- eliminate chromium clock jitter by @felixroos https://codeberg.org/uzu/strudel/pull/1004
|
||||
- Repl sync fixes by @daslyfe https://codeberg.org/uzu/strudel/pull/1014
|
||||
- hotfix for 1017 by @daslyfe https://codeberg.org/uzu/strudel/pull/1020
|
||||
- fix cyclist fizzling out by @felixroos https://codeberg.org/uzu/strudel/pull/1046
|
||||
- Midi Time hotfix for scheduler updates by @daslyfe https://codeberg.org/uzu/strudel/pull/1047
|
||||
- fix: do not reset cc input values on each eval by @felixroos https://codeberg.org/uzu/strudel/pull/1054
|
||||
- Fix wchooseCycles not picking the whole pattern by @ilesinge https://codeberg.org/uzu/strudel/pull/1061
|
||||
- fix OSC timing for recent scheduler updates by @daslyfe https://codeberg.org/uzu/strudel/pull/1062
|
||||
- clarify license by @yaxu https://codeberg.org/uzu/strudel/pull/1064
|
||||
- fix failing format test by @daslyfe https://codeberg.org/uzu/strudel/pull/1077
|
||||
- fix: url parsing with extra params by @felixroos https://codeberg.org/uzu/strudel/pull/1083
|
||||
- fix: csound + dough timing by @felixroos https://codeberg.org/uzu/strudel/pull/1086
|
||||
- fix: missing events due to premature worklet cleanup by @felixroos https://codeberg.org/uzu/strudel/pull/1089
|
||||
- Use sessionStorage for viewingPatternData and activePattern by @kasparsj https://codeberg.org/uzu/strudel/pull/1091
|
||||
- osc: couple of fixes by @kasparsj https://codeberg.org/uzu/strudel/pull/1093
|
||||
- web package fixes by @felixroos https://codeberg.org/uzu/strudel/pull/1044
|
||||
- Fix audio worklets by @daslyfe https://codeberg.org/uzu/strudel/pull/1114
|
||||
- fix: use full repl in web package by @felixroos https://codeberg.org/uzu/strudel/pull/1119
|
||||
- [BUG FIX] Audio worklets sometimes dont load by @daslyfe https://codeberg.org/uzu/strudel/pull/1121
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @mhetrick made their first contribution https://codeberg.org/uzu/strudel/pull/941
|
||||
- @eefano made their first contribution https://codeberg.org/uzu/strudel/pull/956
|
||||
- @Enelg52 made their first contribution https://codeberg.org/uzu/strudel/pull/936
|
||||
- @andresgottlieb made their first contribution https://codeberg.org/uzu/strudel/pull/984
|
||||
- @cleary made their first contribution https://codeberg.org/uzu/strudel/pull/1068
|
||||
- @diegodorado made their first contribution https://codeberg.org/uzu/strudel/pull/1076
|
||||
- @lukad made their first contribution https://codeberg.org/uzu/strudel/pull/1104
|
||||
- @giohappy made their first contribution https://codeberg.org/uzu/strudel/pull/1116
|
||||
|
||||
A huge thanks to all contributors!!!
|
||||
|
||||
## Packages
|
||||
|
||||
- @strudel/codemirror@1.1.0
|
||||
- @strudel/core@1.1.0
|
||||
- @strudel/csound@1.1.0
|
||||
- @strudel/draw@1.1.0
|
||||
- @strudel/embed@1.1.0
|
||||
- hs2js@0.1.0
|
||||
- @strudel/hydra@1.1.0
|
||||
- @strudel/midi@1.1.0
|
||||
- @strudel/mini@1.1.0
|
||||
- @strudel/osc@1.1.0
|
||||
- @strudel/repl@1.1.0
|
||||
- @strudel/sampler@0.1.0
|
||||
- @strudel/serial@1.1.0
|
||||
- @strudel/soundfonts@1.1.0
|
||||
- superdough@1.1.0
|
||||
- @strudel/tidal@0.1.0
|
||||
- @strudel/tonal@1.1.0
|
||||
- @strudel/transpiler@1.1.0
|
||||
- @strudel/web@1.1.0
|
||||
- @strudel/webaudio@1.1.0
|
||||
- @strudel/xen@1.1.0
|
||||
|
||||
**Full Changelog**: https://codeberg.org/uzu/strudel/compare/v1.0.0...v1.1.0
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: 'Release Notes v1.2.0'
|
||||
description: ''
|
||||
date: '2025-05-01'
|
||||
tags: ['meta']
|
||||
author: froos
|
||||
---
|
||||
|
||||
## What's Changed
|
||||
|
||||
## highlights
|
||||
|
||||
- [stepwise functions](https://strudel.cc/learn/stepwise/) ([PR](https://github.com/tidalcycles/strudel/pull/1262))
|
||||
- [midimaps](https://strudel.cc/learn/input-output/#midimaps) ([PR](https://github.com/tidalcycles/strudel/pull/1274))
|
||||
- [spectrum](https://strudel.cc/learn/visual-feedback/#spectrum) ([PR](https://github.com/tidalcycles/strudel/pull/1213))
|
||||
- [mqtt](https://strudel.cc/learn/input-output/#mqtt) ([PR](https://github.com/tidalcycles/strudel/pull/1224))
|
||||
- pulse oscillator (todo: https://github.com/tidalcycles/strudel/issues/1336) ([PR](https://github.com/tidalcycles/strudel/pull/1304))
|
||||
- theme improvements
|
||||
|
||||
## breaking changes
|
||||
|
||||
- [breaking change] Sample signals from query onset, rather than midpoint by @yaxu in https://github.com/tidalcycles/strudel/pull/1278
|
||||
- change behaviour of polymeter, and remove polymeterSteps by @yaxu in https://github.com/tidalcycles/strudel/pull/1302
|
||||
- Polish, rename, and document stepwise functions by @yaxu in https://github.com/tidalcycles/strudel/pull/1262
|
||||
|
||||
### superdough
|
||||
|
||||
- feat: Create Pulse Oscillator with variable PWM by @daslyfe in https://github.com/tidalcycles/strudel/pull/1304
|
||||
- add num samples (edited numbers) by @yaxu in https://github.com/tidalcycles/strudel/pull/1309
|
||||
- Add num samples from 0 up to 20 by @yaxu in https://github.com/tidalcycles/strudel/pull/1310
|
||||
- feat: add max polyphony feature for superdough by @daslyfe in https://github.com/tidalcycles/strudel/pull/1317
|
||||
|
||||
### docs
|
||||
|
||||
- doc: visual functions + refactor onPaint by @felixroos in https://github.com/tidalcycles/strudel/pull/1125
|
||||
- Labeled statements doc by @felixroos in https://github.com/tidalcycles/strudel/pull/1126
|
||||
- Correct spelling mistakes by @EdwardBetts in https://github.com/tidalcycles/strudel/pull/1183
|
||||
- remove redundant example for cat, update snapshot by @kdiab in https://github.com/tidalcycles/strudel/pull/1189
|
||||
- chore: Edit run locally instructions in README.md by @ChinoUkaegbu in https://github.com/tidalcycles/strudel/pull/1206
|
||||
- suggested changes to voicings.mdx by @bwagner in https://github.com/tidalcycles/strudel/pull/1231
|
||||
- Documentation for all/each, and bugfix for each by @yaxu in https://github.com/tidalcycles/strudel/pull/1233
|
||||
- Update documentation for param value modification by @gillespi314 in https://github.com/tidalcycles/strudel/pull/1238
|
||||
- fix docs for beat function by @daslyfe in https://github.com/tidalcycles/strudel/pull/1248
|
||||
- understand voicings page by @felixroos in https://github.com/tidalcycles/strudel/pull/1230
|
||||
- add reference package by @felixroos in https://github.com/tidalcycles/strudel/pull/1252
|
||||
- Stepwise documentation tweaks, with mridangam samples by @yaxu in https://github.com/tidalcycles/strudel/pull/1275
|
||||
- showcase tweaks by @yaxu in https://github.com/tidalcycles/strudel/pull/1291
|
||||
- Signpost licenses for source code and samples a bit more, ref #1277 by @yaxu in https://github.com/tidalcycles/strudel/pull/1289
|
||||
- Fix misplaced ending sentence by @makmanalp in https://github.com/tidalcycles/strudel/pull/1296
|
||||
- Fix typo pattnr by @ReneNyffenegger in https://github.com/tidalcycles/strudel/pull/1316
|
||||
- update docs to reflect import sounds tab change by @hpunq in https://github.com/tidalcycles/strudel/pull/1332
|
||||
|
||||
### ui improvements
|
||||
|
||||
- Udels (MultiFrame Strudel) Revisited by @daslyfe in https://github.com/tidalcycles/strudel/pull/1132
|
||||
- Create audio target selector for OSC/Superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1160
|
||||
- Add a search bar to the REPL Reference tab by @netux in https://github.com/tidalcycles/strudel/pull/1165
|
||||
- Adding search bar (soundtab.jsx) by @Bubobubobubobubo in https://github.com/tidalcycles/strudel/pull/1185
|
||||
- add 2 new ui settings by @felixroos in https://github.com/tidalcycles/strudel/pull/1200
|
||||
- Theme glowup by @felixroos in https://github.com/tidalcycles/strudel/pull/1268
|
||||
- Create Pattern Page Pagination by @daslyfe in https://github.com/tidalcycles/strudel/pull/1287
|
||||
- feat: Theme improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/1295
|
||||
- feat: new themes + theme improvements by @daslyfe in https://github.com/tidalcycles/strudel/pull/1326
|
||||
- Add new "import-sounds" tab with explanation on folder import by @hpunq in https://github.com/tidalcycles/strudel/pull/1329
|
||||
- Add Icon to import sample button by @daslyfe in https://github.com/tidalcycles/strudel/pull/1331
|
||||
- better spacing in zen mode by @felixroos in https://github.com/tidalcycles/strudel/pull/1147
|
||||
- Screenreader improvements by @yaxu in https://github.com/tidalcycles/strudel/pull/1158
|
||||
- colorize console + tweak header by @felixroos in https://github.com/tidalcycles/strudel/pull/1203
|
||||
- Menu Panel Improvements! by @daslyfe in https://github.com/tidalcycles/strudel/pull/1193
|
||||
- Make panel hover behavior optional by @daslyfe in https://github.com/tidalcycles/strudel/pull/1199
|
||||
- REPL: solo and sync configuration by @bthj in https://github.com/tidalcycles/strudel/pull/1214
|
||||
- enhancement: make error messages easier to read by @daslyfe in https://github.com/tidalcycles/strudel/pull/1315
|
||||
|
||||
### mqtt
|
||||
|
||||
- MQTT support by @yaxu in https://github.com/tidalcycles/strudel/pull/1224
|
||||
- MQTT - if password isn't provided, prompt for one by @yaxu in https://github.com/tidalcycles/strudel/pull/1249
|
||||
- MQTT - support adding hap duration and cps metadata to JSON messages by @yaxu in https://github.com/tidalcycles/strudel/pull/1279
|
||||
- make mqtt topic patternable by @yaxu in https://github.com/tidalcycles/strudel/pull/1280
|
||||
- Bugfix: update mqtt connections dictionary by @yaxu in https://github.com/tidalcycles/strudel/pull/1281
|
||||
- mqtt bugfix - connection check by @yaxu in https://github.com/tidalcycles/strudel/pull/1282
|
||||
|
||||
### new functions
|
||||
|
||||
- Add scramble and shuffle by @yaxu in https://github.com/tidalcycles/strudel/pull/1167
|
||||
- polyJoin by @yaxu in https://github.com/tidalcycles/strudel/pull/1168
|
||||
- Add seqPLoop from Tidal by @yaxu in https://github.com/tidalcycles/strudel/pull/1182
|
||||
- add filter + filterWhen + within by @felixroos in https://github.com/tidalcycles/strudel/pull/1039
|
||||
- Add bite function by @yaxu in https://github.com/tidalcycles/strudel/pull/1187
|
||||
- markcss by @felixroos in https://github.com/tidalcycles/strudel/pull/1202
|
||||
- "beat" function for "step sequencer" style rhythm notation by @daslyfe in https://github.com/tidalcycles/strudel/pull/1237
|
||||
- Add s_zip for 'cat'-ing patterns together step-by-step, bugfix `steps` by @yaxu in https://github.com/tidalcycles/strudel/pull/1208
|
||||
- "stretch" function (phase vocoder) by @daslyfe in https://github.com/tidalcycles/strudel/pull/1130
|
||||
- add basic spectrum function by @felixroos in https://github.com/tidalcycles/strudel/pull/1213
|
||||
- Add onKey function for custom key commands for patterns by @daslyfe in https://github.com/tidalcycles/strudel/pull/1235
|
||||
- Add binary and binaryN by @heerman in https://github.com/tidalcycles/strudel/pull/1226
|
||||
- midimaps by @felixroos in https://github.com/tidalcycles/strudel/pull/1274
|
||||
- small feat: Add alias for segment and ribbon by @daslyfe in https://github.com/tidalcycles/strudel/pull/1314
|
||||
- feat: Create scrub function for scrubbing an audio file by @daslyfe in https://github.com/tidalcycles/strudel/pull/1321
|
||||
- feat: Improve gain curve by @daslyfe in https://github.com/tidalcycles/strudel/pull/1318
|
||||
- Chop chop by @yaxu in https://github.com/tidalcycles/strudel/pull/1078
|
||||
|
||||
### more
|
||||
|
||||
- Make `all()` post-stack again, and add `each()` for pre-stack by @yaxu in https://github.com/tidalcycles/strudel/pull/1229
|
||||
- Add stepBind, and some toplevel aliases for binds and withValue by @yaxu in https://github.com/tidalcycles/strudel/pull/1241
|
||||
- Make cps patternable by @eefano in https://github.com/tidalcycles/strudel/pull/1001
|
||||
- Allow wchooseCycles probabilities to be patterned by @yaxu in https://github.com/tidalcycles/strudel/pull/1292
|
||||
- @strudel/sampler improvements by @felixroos in https://github.com/tidalcycles/strudel/pull/1288
|
||||
|
||||
### refactor
|
||||
|
||||
- expose comment commands by @felixroos in https://github.com/tidalcycles/strudel/pull/1136
|
||||
- containerize/seperate out boolean checks for repl types/Repl logic into bespoke components. by @daslyfe in https://github.com/tidalcycles/strudel/pull/1163
|
||||
- Improve + simplify neocyclist timing by @daslyfe in https://github.com/tidalcycles/strudel/pull/1164
|
||||
- Make phaser control consistent with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1178
|
||||
- Revert "Make phaser control consistent with superdirt" by @daslyfe in https://github.com/tidalcycles/strudel/pull/1179
|
||||
- make phaser control match superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1180
|
||||
- refactor sampler by @felixroos in https://github.com/tidalcycles/strudel/pull/1101
|
||||
- update lockfile + minor versions by @felixroos in https://github.com/tidalcycles/strudel/pull/1198
|
||||
- Preserve tactus for 'degrade' and friends, and tidy up 'pick' and friends by @yaxu in https://github.com/tidalcycles/strudel/pull/1205
|
||||
- Apply `all` function to individual patterns rather than final stack by @yaxu in https://github.com/tidalcycles/strudel/pull/1209
|
||||
- Revert "Fix sometimes" by @yaxu in https://github.com/tidalcycles/strudel/pull/1267
|
||||
- patchday by @felixroos in https://github.com/tidalcycles/strudel/pull/1264
|
||||
- Rename repeat back to extend by @yaxu in https://github.com/tidalcycles/strudel/pull/1285
|
||||
- Send delta in OSC message in seconds, to match tidal/superdirt by @yaxu in https://github.com/tidalcycles/strudel/pull/1323
|
||||
|
||||
### fixes
|
||||
|
||||
- Fix clock worker dependency path in module builds by @matthewkaney in https://github.com/tidalcycles/strudel/pull/1129
|
||||
- Fix bug in Fraction.lcm by @yaxu in https://github.com/tidalcycles/strudel/pull/1133
|
||||
- Fix tactus marking in mininotation by @yaxu in https://github.com/tidalcycles/strudel/pull/1144
|
||||
- Fix loopAt tactus by @yaxu in https://github.com/tidalcycles/strudel/pull/1145
|
||||
- Fix OSC clock jitter by @daslyfe in https://github.com/tidalcycles/strudel/pull/1157
|
||||
- [CORS HOTFIX] by @daslyfe in https://github.com/tidalcycles/strudel/pull/1162
|
||||
- Fixes fit so it works after a chop or slice by @yaxu in https://github.com/tidalcycles/strudel/pull/1171
|
||||
- fix sample speed when using splice and fit with superdirt by @daslyfe in https://github.com/tidalcycles/strudel/pull/1172
|
||||
- handle midin device not found error by @felixroos in https://github.com/tidalcycles/strudel/pull/1146
|
||||
- Fix serial timing by @yaxu in https://github.com/tidalcycles/strudel/pull/1188
|
||||
- Fix regression for d1, p1, p(n) by @yaxu in https://github.com/tidalcycles/strudel/pull/1227
|
||||
- Fix sometimes by @yaxu in https://github.com/tidalcycles/strudel/pull/1243
|
||||
- Fix sf2 timing by @felixroos in https://github.com/tidalcycles/strudel/pull/1272
|
||||
- Fix "squeezejoin" and functions using it, including "bite" by @yaxu in https://github.com/tidalcycles/strudel/pull/1286
|
||||
- Fixes inverted triangle wave by renaming it to "itri", making non-inverted "tri" by @yaxu in https://github.com/tidalcycles/strudel/pull/1283
|
||||
- Hotfix: prevent undefined pattern code from crashing strudel on load by @daslyfe in https://github.com/tidalcycles/strudel/pull/1297
|
||||
- Fix test error #1297 by @nkymut in https://github.com/tidalcycles/strudel/pull/1298
|
||||
- bugfix zoom stepcount by @yaxu in https://github.com/tidalcycles/strudel/pull/1301
|
||||
- bugfix: Allow single param to be used in the as function by @daslyfe in https://github.com/tidalcycles/strudel/pull/1312
|
||||
- fix: replace empty spaces in registered sound keys by @daslyfe in https://github.com/tidalcycles/strudel/pull/1319
|
||||
- FIX: Multichannel Audio by @daslyfe in https://github.com/tidalcycles/strudel/pull/1322
|
||||
- fix: udels header by @daslyfe in https://github.com/tidalcycles/strudel/pull/1325
|
||||
- fix: disable astro toolbar by default by @daslyfe in https://github.com/tidalcycles/strudel/pull/1324
|
||||
- FIX: sound import order by @daslyfe in https://github.com/tidalcycles/strudel/pull/1333`
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @EdwardBetts made their first contribution in https://github.com/tidalcycles/strudel/pull/1183
|
||||
- @netux made their first contribution in https://github.com/tidalcycles/strudel/pull/1165
|
||||
- @kdiab made their first contribution in https://github.com/tidalcycles/strudel/pull/1189
|
||||
|
||||
**Full Changelog**: https://github.com/tidalcycles/strudel/compare/v1.1.0...v1.1.1
|
||||
|
||||
## packages
|
||||
|
||||
- @strudel/codemirror@1.2.0
|
||||
- @strudel/core@1.2.0
|
||||
- @strudel/csound@1.2.0
|
||||
- @strudel/draw@1.2.0
|
||||
- @strudel/gamepad@1.2.0
|
||||
- @strudel/hydra@1.2.0
|
||||
- @strudel/midi@1.2.0
|
||||
- @strudel/mini@1.2.0
|
||||
- @strudel/motion@1.2.0
|
||||
- @strudel/mqtt@1.2.0
|
||||
- @strudel/osc@1.2.0
|
||||
- @strudel/reference@1.2.0
|
||||
- @strudel/repl@1.2.0
|
||||
- @strudel/sampler@0.2.0
|
||||
- @strudel/serial@1.2.0
|
||||
- @strudel/soundfonts@1.2.0
|
||||
- superdough@1.2.0
|
||||
- @strudel/tonal@1.2.0
|
||||
- @strudel/transpiler@1.2.0
|
||||
- @strudel/web@1.2.0
|
||||
- @strudel/webaudio@1.2.0
|
||||
- @strudel/xen@1.2.0
|
||||
@@ -30,6 +30,7 @@ export function MiniRepl({
|
||||
maxHeight,
|
||||
autodraw,
|
||||
drawTime,
|
||||
mondo = false,
|
||||
}) {
|
||||
const code = tunes ? tunes[0] : tune;
|
||||
const id = useMemo(() => s4(), []);
|
||||
@@ -85,6 +86,7 @@ export function MiniRepl({
|
||||
},
|
||||
beforeStart: () => audioReady,
|
||||
afterEval: ({ code }) => setVersionDefaultsFrom(code),
|
||||
mondo,
|
||||
});
|
||||
// init settings
|
||||
editor.setCode(code);
|
||||
|
||||
@@ -95,7 +95,7 @@ Diese Kombinationen von Buchstaben stehen für verschiedene Teile eines Schlagze
|
||||
- `mt` = **m**iddle tom
|
||||
- `ht` = **h**igh tom
|
||||
- `rd` = **r**i**d**e cymbal
|
||||
- `rd` = **cr**ash cymbal
|
||||
- `cr` = **cr**ash cymbal
|
||||
|
||||
Probier verschiedene Sounds aus!
|
||||
|
||||
@@ -168,9 +168,14 @@ Korrekt, der echte Vorteil dieser Schreibweise zeigt sich wenn du Elemente entfe
|
||||
|
||||
</Box>
|
||||
|
||||
**Tempo ändern mit `cpm`**
|
||||
**Tempo ändern mit `setcpm`**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd hh rim hh>*8").cpm(90/4)`} punchcard />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(90/4)
|
||||
sound("<bd hh rim hh>*8")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -233,8 +238,9 @@ Bolero:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound("sd sd*3 sd sd*3 sd sd sd sd*3 sd sd*3 sd*3 sd*3")
|
||||
.cpm(10)`}
|
||||
tune={`
|
||||
setcpm(10)
|
||||
sound("sd sd*3 sd sd*3 sd sd sd sd*3 sd sd*3 sd*3 sd*3")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -313,18 +319,23 @@ Das haben wir bisher gelernt:
|
||||
Die mit Apostrophen umgebene Mini-Notation benutzt man normalerweise in einer sogenannten Funktion.
|
||||
Die folgenden Funktionen haben wir bereits gesehen:
|
||||
|
||||
| Name | Description | Example |
|
||||
| ----- | -------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| sound | Spielt den Sound mit dem Namen | <MiniRepl client:visible tune={`sound("bd sd")`} /> |
|
||||
| bank | Wählt die Soundbank / Drum Machine | <MiniRepl client:visible tune={`sound("bd sd").bank("RolandTR909")`} /> |
|
||||
| cpm | Tempo in **C**ycles **p**ro **M**inute | <MiniRepl client:visible tune={`sound("bd sd").cpm(90)`} /> |
|
||||
| n | Sample Nummer | <MiniRepl client:visible tune={`n("0 1 4 2").sound("jazz")`} /> |
|
||||
| Name | Description | Example |
|
||||
| ------ | -------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| sound | Spielt den Sound mit dem Namen | <MiniRepl client:visible tune={`sound("bd sd")`} /> |
|
||||
| bank | Wählt die Soundbank / Drum Machine | <MiniRepl client:visible tune={`sound("bd sd").bank("RolandTR909")`} /> |
|
||||
| setcpm | Tempo in **C**ycles **p**ro **M**inute | <MiniRepl client:visible tune={`setcpm(90); sound("bd sd")`} /> |
|
||||
| n | Sample Nummer | <MiniRepl client:visible tune={`n("0 1 4 2").sound("jazz")`} /> |
|
||||
|
||||
## Beispiele
|
||||
|
||||
**Einfacher Rock Beat**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd sd, hh*4").bank("RolandTR505").cpm(100/2)`} punchcard />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(100/2)
|
||||
sound("bd sd, hh*4").bank("RolandTR505")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
**Klassischer House**
|
||||
|
||||
@@ -339,7 +350,12 @@ Bestimmte Drum Patterns werden oft genreübergreifend wiederverwendet.
|
||||
|
||||
We Will Rock you
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd*2 cp").bank("RolandTR707").cpm(81/2)`} punchcard />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(81/2)
|
||||
sound("bd*2 cp").bank("RolandTR707")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
**Yellow Magic Orchestra - Firecracker**
|
||||
|
||||
@@ -354,12 +370,13 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
tune={`setcpm(90/4)
|
||||
sound(\`
|
||||
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
|
||||
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
|
||||
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
|
||||
\`).cpm(90/4)`}
|
||||
\`)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -367,12 +384,13 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
tune={`setcpm(88/4)
|
||||
sound(\`
|
||||
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
|
||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [- cp - - ],
|
||||
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
|
||||
\`).bank("RolandTR808").cpm(88/4)`}
|
||||
\`).bank("RolandTR808")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -380,11 +398,11 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s(\`jazz*2,
|
||||
tune={`setcpm(100/2)
|
||||
s(\`jazz*2,
|
||||
insect [crow metal] - -,
|
||||
- space:4 - space:1,
|
||||
- wind\`)
|
||||
.cpm(100/2)`}
|
||||
- wind\`)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
|
||||
@@ -56,13 +56,13 @@ Diese Seite ist eine Auflistung aller im Workshop vorgestellten Funktionen.
|
||||
|
||||
## Pattern-Effekte
|
||||
|
||||
| Name | Beschreibung | Beispiel |
|
||||
| ---- | --------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| cpm | Tempo in Cycles pro Minute | <MiniRepl client:visible tune={`sound("bd sd").cpm(90)`} /> |
|
||||
| fast | schneller | <MiniRepl client:visible tune={`sound("bd sd").fast(2)`} /> |
|
||||
| slow | langsamer | <MiniRepl client:visible tune={`sound("bd sd").slow(2)`} /> |
|
||||
| rev | rückwärts | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
|
||||
| jux | einen Stereo-Kanal modifizieren | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
|
||||
| add | addiert Zahlen oder Noten | <MiniRepl client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
|
||||
| ply | jedes Element schneller machen | <MiniRepl client:visible tune={`s("bd sd").ply("<1 2 3>")`} /> |
|
||||
| off | verzögert eine modifizierte Kopie | <MiniRepl client:visible tune={`s("bd sd, hh*4").off(1/8, x=>x.speed(2))`} /> |
|
||||
| Name | Beschreibung | Beispiel |
|
||||
| ------ | --------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| setcpm | Tempo in Cycles pro Minute | <MiniRepl client:visible tune={`setcpm(90); sound("bd sd")`} /> |
|
||||
| fast | schneller | <MiniRepl client:visible tune={`sound("bd sd").fast(2)`} /> |
|
||||
| slow | langsamer | <MiniRepl client:visible tune={`sound("bd sd").slow(2)`} /> |
|
||||
| rev | rückwärts | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").rev()`} /> |
|
||||
| jux | einen Stereo-Kanal modifizieren | <MiniRepl client:visible tune={`n("0 2 4 6").scale("C:minor").jux(rev)`} /> |
|
||||
| add | addiert Zahlen oder Noten | <MiniRepl client:visible tune={`n("0 2 4 6".add("<0 1 2 1>")).scale("C:minor")`} /> |
|
||||
| ply | jedes Element schneller machen | <MiniRepl client:visible tune={`s("bd sd").ply("<1 2 3>")`} /> |
|
||||
| off | verzögert eine modifizierte Kopie | <MiniRepl client:visible tune={`s("bd sd, hh*4").off(1/8, x=>x.speed(2))`} /> |
|
||||
|
||||
@@ -57,6 +57,34 @@ Each filter has 2 parameters:
|
||||
|
||||
<JsDoc client:idle name="vowel" h={0} />
|
||||
|
||||
# Amplitude Modulation
|
||||
|
||||
Amplitude modulation changes the amplitude (gain) periodically over time.
|
||||
|
||||
## am
|
||||
|
||||
<JsDoc client:idle name="am" h={0} />
|
||||
|
||||
## tremolosync
|
||||
|
||||
<JsDoc client:idle name="tremolosync" h={0} />
|
||||
|
||||
## tremolodepth
|
||||
|
||||
<JsDoc client:idle name="tremolodepth" h={0} />
|
||||
|
||||
## tremoloskew
|
||||
|
||||
<JsDoc client:idle name="tremoloskew" h={0} />
|
||||
|
||||
## tremolophase
|
||||
|
||||
<JsDoc client:idle name="tremolophase" h={0} />
|
||||
|
||||
## tremoloshape
|
||||
|
||||
<JsDoc client:idle name="tremoloshape" h={0} />
|
||||
|
||||
# Amplitude Envelope
|
||||
|
||||
The amplitude [envelope](<https://en.wikipedia.org/wiki/Envelope_(music)>) controls the dynamic contour of a sound.
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Mondo Notation
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Mondo Notation
|
||||
|
||||
"Mondo Notation" is a new kind of notation that is similar to [Mini Notation](/learn/mini-notation/), but with enough abilities to make it work as a standalone pattern language.
|
||||
Here's an example:
|
||||
|
||||
<MiniRepl
|
||||
mondo
|
||||
client:idle
|
||||
tune={`$ note (c2 # euclid <3 6 3> <8 16>) # *2
|
||||
# s "sine" # add (note [0 <12 24>]*2)
|
||||
# dec(sine # range .2 2)
|
||||
# room .5
|
||||
# lpf (sine/3 # range 120 400)
|
||||
# lpenv (rand # range .5 4)
|
||||
# lpq (perlin # range 5 12 # * 2)
|
||||
# dist 1 # fm 4 # fmh 5.01 # fmdecay <.1 .2>
|
||||
# postgain .6 # delay .1 # clip 5
|
||||
|
||||
$ s [bd bd bd bd] # bank tr909 # clip .5
|
||||
|
||||
# ply <1 [1 [2 4]]>
|
||||
|
||||
$ s oh\*4 # press # bank tr909 # speed.8
|
||||
|
||||
# dec (<.02 .05>\*2 # add (saw/8 # range 0 1))
|
||||
|
||||
`}
|
||||
/>
|
||||
|
||||
## Mondo in the REPL
|
||||
|
||||
For now, you can only use mondo in the repl like this:
|
||||
|
||||
<MiniRepl client:idle tune={'mondo`s hh*8`'} />
|
||||
|
||||
The rest of this site will only use the mondo notation itself.
|
||||
In the future, the REPL might get a way to use mondo notation directly.
|
||||
|
||||
## Calling Functions
|
||||
|
||||
Compared to Mini Notation, the most notable feature of Mondo Notation is the ability to call functions using round brackets:
|
||||
|
||||
<MiniRepl mondo client:idle tune={`(s hh*8)`} />
|
||||
|
||||
The first element inside the brackets is the function name. In JS, this would look like:
|
||||
|
||||
<MiniRepl client:idle tune={`s("hh*8")`} />
|
||||
|
||||
The outermost parens are not needed, so we can drop them:
|
||||
|
||||
<MiniRepl mondo client:idle tune={`s hh*8`} />
|
||||
|
||||
## Mini Notation Features
|
||||
|
||||
Besides function calling with round parens, Mondo Notation has a lot in common with Mini Notation:
|
||||
|
||||
### Brackets
|
||||
|
||||
- `[]` for 1-cycle sequences
|
||||
- `<>` for multi-cycle sequences
|
||||
- `{}` for stepped sequences (more on that later)
|
||||
|
||||
### Infix Operators
|
||||
|
||||
- \* => [fast](/learn/time-modifiers/#fast)
|
||||
- / => [slow](/learn/time-modifiers/#slow)
|
||||
- ! => [extend](/learn/stepwise/#extend)
|
||||
- @ => [expand](/learn/stepwise/#expand)
|
||||
- % => [pace](/learn/stepwise/#pace)
|
||||
- ? => [degradeBy](/learn/random-modifiers/#degradeby) (currently requires right operand)
|
||||
- : => tail (creates a list)
|
||||
- .. => range (between numbers)
|
||||
- , => [stack](/learn/factories/#stack)
|
||||
- | => [chooseIn](/learn/random-modifiers/#choose)
|
||||
|
||||
### Example
|
||||
|
||||
<MiniRepl
|
||||
mondo
|
||||
client:idle
|
||||
tune={`note <
|
||||
[e5 [b4 c5] d5 [c5 b4]]
|
||||
[a4 [a4 c5] e5 [d5 c5]]
|
||||
[b4 [~ c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||
[e5 [~ c5] e5 [d5 c5]]
|
||||
[b4 [b4 c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
>`}
|
||||
/>
|
||||
|
||||
## Chaining Functions
|
||||
|
||||
Similar to how "." works in javascript (JS), we can chain functions calls with the "#" operator:
|
||||
|
||||
<MiniRepl
|
||||
mondo
|
||||
client:idle
|
||||
tune={`n <0 2 4 [3 1] -1>*4
|
||||
# scale C4:minor
|
||||
# jux rev
|
||||
# dec .2
|
||||
# delay .5`}
|
||||
/>
|
||||
|
||||
Here's the same written in JS:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`n("<0 2 4 [3 1] -1>*4")
|
||||
.scale("C4:minor")
|
||||
.jux(rev)
|
||||
.dec(.2)
|
||||
.delay(.5)`}
|
||||
/>
|
||||
|
||||
### Chaining Functions Locally
|
||||
|
||||
A function can be applied to a single element by wrapping it in round parens:
|
||||
|
||||
<MiniRepl mondo client:idle tune={`s [bd hh bd (cp # delay .6)] # bank tr909`} />
|
||||
|
||||
in this case, `delay .6` will only be applied to `cp`. compare this with the JS version:
|
||||
|
||||
<MiniRepl client:idle tune={`s(seq("bd", "hh", "bd", "cp".delay(.6))).bank('tr909')`} />
|
||||
|
||||
here we can see how much we can save when there's no boundary between mini notation and function calls!
|
||||
|
||||
### Chaining Infix Operators
|
||||
|
||||
Infix operators exist as regular functions, so they can be chained as well:
|
||||
|
||||
<MiniRepl client:idle mondo tune={`s [bd hh] # bank tr909 # *2`} />
|
||||
|
||||
In this case, the \*2 will be applied to the whole pattern.
|
||||
|
||||
### Lambda Functions
|
||||
|
||||
Some functions in strudel expect a function as input, for example:
|
||||
|
||||
<MiniRepl client:idle tune={`n("0 .. 7").scale("C:minor").sometimes(x=>x.dec(.1))`} />
|
||||
|
||||
in mondo, the `x=>x.` can be shortened to:
|
||||
|
||||
<MiniRepl client:idle mondo tune={`n 0..7 # scale C:minor # sometimes (# dec .1)`} />
|
||||
|
||||
chaining works as expected:
|
||||
|
||||
<MiniRepl client:idle mondo tune={`n 0..7 # scale C:minor # sometimes (# dec .1 # jux rev)`} />
|
||||
|
||||
## Strings
|
||||
|
||||
You can use "double quotes" and 'single quotes' to get a string:
|
||||
|
||||
<MiniRepl client:idle mondo tune={`n 0..7 # scale 'C minor'`} />
|
||||
|
||||
## Multiple Patterns
|
||||
|
||||
The `$` sign can be used to separate multiple patterns:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
mondo
|
||||
tune={`$ s [bd rim [~ bd] rim] # bank tr707
|
||||
$ chord <Dm9!3 Db7> # voicing
|
||||
# struct[x ~ ~ x ~ x ~ ~] # delay .5`}
|
||||
/>
|
||||
|
||||
The `$` sign is an alias for `,` so it will create a stack behind the scenes.
|
||||
|
||||
## variables
|
||||
|
||||
using the `def` keyword, you can define variables:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
mondo
|
||||
tune={`
|
||||
$ def melody [0 1 2 3]
|
||||
$ n melody # scale C:minor
|
||||
`}
|
||||
/>
|
||||
@@ -163,6 +163,21 @@ The last section could be written as:
|
||||
}
|
||||
```
|
||||
|
||||
Please note that browsers will often cache `strudel.json` on first load, and keep using the cached
|
||||
version even if the orginal has been updated. If this bites you (for example while developing a new
|
||||
sample pack), you can force the browser to download a new copy by i.e. changing capitalization of one
|
||||
character in the URL, or adding a URL attribute, such as:
|
||||
|
||||
```javascript
|
||||
samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json?version=2');
|
||||
```
|
||||
|
||||
that gets ignored by GitHub (but changes the URL, forcing the browser to reload every time we increase
|
||||
the version number).
|
||||
|
||||
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
|
||||
or from the dev console if you're technically minded, or by using a cache deleting extension).
|
||||
|
||||
## Github Shortcut
|
||||
|
||||
Because loading samples from github is common, there is a shortcut:
|
||||
@@ -247,15 +262,17 @@ We can also declare different samples for different regions of the keyboard:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
tune={`setcpm(60)
|
||||
samples({
|
||||
'moog': {
|
||||
'g2': 'moog/004_Mighty%20Moog%20G2.wav',
|
||||
'g3': 'moog/005_Mighty%20Moog%20G3.wav',
|
||||
'g4': 'moog/006_Mighty%20Moog%20G4.wav',
|
||||
}}, 'github:tidalcycles/dirt-samples');
|
||||
}}, 'github:tidalcycles/dirt-samples')
|
||||
|
||||
note("g2!2 <bb2 c3>!2, <c4@3 [<eb4 bb3> g4 f4]>")
|
||||
.s('moog').clip(1)
|
||||
.gain(.5).cpm(60)`}
|
||||
.s('moog').clip(1)
|
||||
.gain(.5)`}
|
||||
/>
|
||||
|
||||
The sampler will always pick the closest matching sample for the current note!
|
||||
|
||||
@@ -43,21 +43,35 @@ This is why the same CPS can produce different perceived tempos.
|
||||
|
||||
## Setting CPM
|
||||
|
||||
If you're familiar with BPM, you can use the `cpm` method to set the tempo in cycles per minute:
|
||||
If you're familiar with BPM, you can use the `setcpm` method to set the global tempo in cycles per minute:
|
||||
|
||||
<MiniRepl client:visible tune={`s("bd hh").cpm(110)`} />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(110)
|
||||
s("bd hh")`}
|
||||
/>
|
||||
|
||||
If you want to add more beats per cycle, you might want to divide the cpm:
|
||||
|
||||
<MiniRepl client:visible tune={`s("bd sd bd rim, hh*8").cpm(110/4)`} />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(110/4)
|
||||
s("bd sd bd rim, hh*8")`}
|
||||
/>
|
||||
|
||||
Or using 2 beats per cycle:
|
||||
|
||||
<MiniRepl client:visible tune={`s("bd sd, hh*4").cpm(110/2)`} />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(110/2)
|
||||
s("bd sd, hh*4")`}
|
||||
/>
|
||||
|
||||
You can use the `setcps` method to set the global tempo in cycles per second. `setcpm(x)` is the same as `setcps(x / 60)`.
|
||||
|
||||
<Box>
|
||||
|
||||
To set a specific bpm, use `.cpm(bpm/bpc)`
|
||||
To set a specific bpm, use `setcpm(bpm/bpc)`
|
||||
|
||||
- bpm: the target beats per minute
|
||||
- bpc: the number of perceived beats per cycle
|
||||
@@ -74,22 +88,31 @@ Many music programs use it as a default.
|
||||
Strudel does not a have concept of bars or measures, there are only cycles.
|
||||
How you use them is up to you. Above, we've had this example:
|
||||
|
||||
<MiniRepl client:visible tune={`s("bd sd bd rim, hh*8").cpm(110/4)`} />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(110/4)
|
||||
s("bd sd bd rim, hh*8")`}
|
||||
/>
|
||||
|
||||
This could be interpreted as 4/4 time with a tempo of 110bpm.
|
||||
We could write out multiple bars like this:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s(\`<
|
||||
tune={`setcpm(110/4)
|
||||
s(\`<
|
||||
[bd sd bd rim, hh*8]
|
||||
[bd sd bd rim*2, hh*8]
|
||||
>\`).cpm(110/4)`}
|
||||
>\`)`}
|
||||
/>
|
||||
|
||||
Instead of writing out each bar separately, we could express this much shorter:
|
||||
|
||||
<MiniRepl client:visible tune={`s("bd <sd rim*<1 2>>,hh*4").cpm(110/2)`} />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(110/2)
|
||||
s("bd <sd rim*<1 2>>,hh*4")`}
|
||||
/>
|
||||
|
||||
Here we can see that thinking in cycles rather than bars simplifies things a lot!
|
||||
These types of simplifications work because of the repetitive nature of rhythm.
|
||||
@@ -109,10 +132,11 @@ We could also write multiple bars with different time signatures:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s(\`<
|
||||
tune={`setcpm(110*2)
|
||||
s(\`<
|
||||
[bd hh rim]@3
|
||||
[bd hh rim sd]@4
|
||||
>\`).cpm(110*2)`}
|
||||
>\`)`}
|
||||
/>
|
||||
|
||||
Here we switch between 3/4 and 4/4, keeping the same tempo.
|
||||
@@ -121,10 +145,11 @@ If we don't specify the length, we get what's called a metric modulation:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s(\`<
|
||||
tune={`setcpm(110/2)
|
||||
s(\`<
|
||||
[bd hh rim]
|
||||
[bd hh rim sd]
|
||||
>\`).cpm(110/2)`}
|
||||
>\`)`}
|
||||
/>
|
||||
|
||||
Now the 3 elements get the same time as the 4 elements, which is why the tempo changes.
|
||||
|
||||
@@ -216,8 +216,9 @@ Finding the right notes can be difficult.. Scales are here to help:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`n("0 2 4 <[6,8] [7,9]>")
|
||||
.scale("C:minor").sound("piano").cpm(60)`}
|
||||
tune={`setcpm(60)
|
||||
n("0 2 4 <[6,8] [7,9]>")
|
||||
.scale("C:minor").sound("piano")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -242,9 +243,10 @@ Just like anything, we can automate the scale with a pattern:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`n("<0 -3>, 2 4 <[6,8] [7,9]>")
|
||||
tune={`setcpm(60)
|
||||
n("<0 -3>, 2 4 <[6,8] [7,9]>")
|
||||
.scale("<C:major D:mixolydian>/4")
|
||||
.sound("piano").cpm(60)`}
|
||||
.sound("piano")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -275,9 +277,10 @@ Try changing that number!
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`n("<[4@2 4] [5@2 5] [6@2 6] [5@2 5]>*2")
|
||||
tune={`setcpm(60)
|
||||
n("<[4@2 4] [5@2 5] [6@2 6] [5@2 5]>*2")
|
||||
.scale("<C2:mixolydian F2:mixolydian>/4")
|
||||
.sound("gm_acoustic_bass").cpm(60)`}
|
||||
.sound("gm_acoustic_bass")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -291,7 +294,12 @@ This is also sometimes called triplet swing. You'll often find it in blues and j
|
||||
|
||||
**Replicate**
|
||||
|
||||
<MiniRepl client:visible tune={`note("c!2 [eb,<g a bb a>]").sound("piano").cpm(60)`} punchcard />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(60)
|
||||
note("c!2 [eb,<g a bb a>]").sound("piano")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
<Box>
|
||||
|
||||
|
||||
@@ -166,9 +166,14 @@ Try also changing the number at the end to change the tempo!
|
||||
|
||||
</Box>
|
||||
|
||||
**changing the tempo with cpm**
|
||||
**changing the tempo with setcpm**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd hh rim hh>*8").cpm(90/4)`} punchcard />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(90/4)
|
||||
sound("<bd hh rim hh>*8")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -283,12 +288,12 @@ This is what we've learned so far:
|
||||
|
||||
The Mini-Notation is usually used inside some function. These are the functions we've seen so far:
|
||||
|
||||
| Name | Description | Example |
|
||||
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd")`} /> |
|
||||
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd").bank("RolandTR909")`} /> |
|
||||
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd").cpm(45)`} /> |
|
||||
| n | select sample number | <MiniRepl client:visible tune={`n("0 1 4 2 0 6 3 2").sound("jazz")`} /> |
|
||||
| Name | Description | Example |
|
||||
| ------ | ----------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd")`} /> |
|
||||
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd").bank("RolandTR909")`} /> |
|
||||
| setcpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`setcpm(45); sound("bd sd [- bd] sd")`} /> |
|
||||
| n | select sample number | <MiniRepl client:visible tune={`n("0 1 4 2 0 6 3 2").sound("jazz")`} /> |
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -296,8 +301,8 @@ The Mini-Notation is usually used inside some function. These are the functions
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound("[bd sd]*2, hh*8").bank("RolandTR505")
|
||||
.cpm(100/4)`}
|
||||
tune={`setcpm(100/4)
|
||||
sound("[bd sd]*2, hh*8").bank("RolandTR505")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -314,14 +319,20 @@ Certain drum patterns are reused across genres.
|
||||
|
||||
We Will Rock you
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd*2 cp").bank("RolandTR707").cpm(81/2)`} punchcard />
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`setcpm(81/2)
|
||||
sound("bd*2 cp").bank("RolandTR707")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
**Yellow Magic Orchestra - Firecracker**
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound("bd sd, - - - hh - hh - -, - perc - perc:1*2")
|
||||
.bank("RolandCompurhythm1000").cpm(120/2)`}
|
||||
tune={`setcpm(120/2)
|
||||
sound("bd sd, - - - hh - hh - -, - perc - perc:1*2")
|
||||
.bank("RolandCompurhythm1000")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -329,12 +340,14 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
tune={`
|
||||
setcpm(90/4)
|
||||
sound(\`
|
||||
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
|
||||
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
|
||||
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
|
||||
\`).cpm(90/4)`}
|
||||
\`)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -342,12 +355,13 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
tune={`setcpm(88/4)
|
||||
sound(\`
|
||||
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
|
||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [~ cp - - ],
|
||||
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
|
||||
\`).bank("RolandTR808").cpm(88/4)`}
|
||||
\`).bank("RolandTR808")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -355,11 +369,11 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s(\`jazz*2,
|
||||
tune={`setcpm(100/2)
|
||||
s(\`jazz*2,
|
||||
insect [crow metal] - -,
|
||||
- space:4 - space:1,
|
||||
- wind\`)
|
||||
.cpm(100/2)`}
|
||||
- wind\`)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
|
||||
@@ -68,9 +68,10 @@ Try commenting out one or more by adding `//` before a line
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`note("c2 [eb3,g3] ".add("<0 <1 -1>>"))
|
||||
tune={`setcpm(60)
|
||||
note("c2 [eb3,g3] ".add("<0 <1 -1>>"))
|
||||
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
|
||||
.sound("gm_acoustic_bass").room(.5).cpm(60)`}
|
||||
.sound("gm_acoustic_bass").room(.5)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -84,9 +85,10 @@ We can add as often as we like:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`note("c2 [eb3,g3]".add("<0 <1 -1>>").add("0,7"))
|
||||
tune={`setcpm(60)
|
||||
note("c2 [eb3,g3]".add("<0 <1 -1>>").add("0,7"))
|
||||
.color("<cyan <magenta yellow>>").adsr("[.1 0]:.2:[1 0]")
|
||||
.sound("gm_acoustic_bass").room(.5).cpm(60)`}
|
||||
.sound("gm_acoustic_bass").room(.5)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user