mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
383 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0c35e943f | |||
| 4c35f6a77b | |||
| e92f4938cd | |||
| 12204c9053 | |||
| 980bf17059 | |||
| f2009a33aa | |||
| a65a68b828 | |||
| cbe7aaacfb | |||
| 35014b5190 | |||
| 9ff0449ca3 | |||
| 8398385dcd | |||
| 629c2b9301 | |||
| 5cc93996ce | |||
| 7cef558440 | |||
| c261fb6dd6 | |||
| 28056deb66 | |||
| 7071dec775 | |||
| 3d55567445 | |||
| 39e2221c29 | |||
| b8ce920f50 | |||
| ccb5ec1685 | |||
| b09230fe90 | |||
| 8f7b439dce | |||
| c823a37c93 | |||
| 0c9f535ff5 | |||
| fb5884ab81 | |||
| ea91d30ce4 | |||
| f0bd5bffaa | |||
| ac37b6a029 | |||
| a986688874 | |||
| 4ab8c256b2 | |||
| 06ea5a36e2 | |||
| 98f5a56ef6 | |||
| 46af6ed9ef | |||
| 752940b3ab | |||
| c727534d29 | |||
| 20e5fdedfb | |||
| 188f0e9098 | |||
| c2a5562bad | |||
| 57f2a97b7b | |||
| 8b26f784d7 | |||
| 72423c3a25 | |||
| 105db2a4ca | |||
| be61d08cc6 | |||
| 6e58c973af | |||
| 2473a5391d | |||
| c02def03b6 | |||
| 4e17cfbdd6 | |||
| 83c7e63432 | |||
| 909e0154fe | |||
| ef4e21ac40 | |||
| 857c82e260 | |||
| 0d5dc1f8c1 | |||
| ba93bd1bc5 | |||
| 3c1a8c8bdb | |||
| b7e941c649 | |||
| ded91fab7d | |||
| f40d079543 | |||
| d5833dd475 | |||
| e3658ec30e | |||
| 72eaf96b4f | |||
| 9c5c71c31a | |||
| af53bab259 | |||
| 0065db8569 | |||
| 7eebf3a466 | |||
| 6f703e48e7 | |||
| 64f16c4f33 | |||
| ffbbc43c89 | |||
| 086596c47c | |||
| fdde05e751 | |||
| 694162a7b1 | |||
| 319a2d7289 | |||
| 3a7d50924d | |||
| cbcb25b063 | |||
| f33db5f07f | |||
| b7fa440bda | |||
| 3dc9b3524c | |||
| f48f8e46e5 | |||
| a79fc3db5b | |||
| 30a2e14959 | |||
| cfe17fcba5 | |||
| 46ba165c7e | |||
| 73a9834959 | |||
| be9368e8a2 | |||
| 1ae773acc4 | |||
| 39a73f2499 | |||
| 53c504f260 | |||
| 0d08225983 | |||
| 00872bd1de | |||
| 11dd8a318a | |||
| 3ef6c7c921 | |||
| 120f89d57f | |||
| d496919fe5 | |||
| f369de13d7 | |||
| 908a703b7f | |||
| 99e14dae5c | |||
| ac582b4d40 | |||
| b7d98dd370 | |||
| 0ecacae71e | |||
| 8cd497abb2 | |||
| 070055d2ef | |||
| e500ebe268 | |||
| 72298c5f03 | |||
| c885d84785 | |||
| a14bf4d97b | |||
| c5f1085bd9 | |||
| ccbf28f2db | |||
| 3a7ec18d46 | |||
| a2b8407fbb | |||
| ac1fd88d8d | |||
| 9f8143e062 | |||
| cdc5e5f3f3 | |||
| bad498a1e5 | |||
| 4de0c11f8c | |||
| 97af50222f | |||
| f360d394a7 | |||
| 48718daea8 | |||
| 8d45016ccf | |||
| ec69f6fd77 | |||
| d94f145649 | |||
| bf3fe605d9 | |||
| 54ebc97ddc | |||
| c689874bb4 | |||
| 23d4bfa9d9 | |||
| 91eb50f1c1 | |||
| 5838ade740 | |||
| 917d01c47d | |||
| 0a51718df3 | |||
| 774372a339 | |||
| 626a99ba5b | |||
| 8c3ee6db6b | |||
| c5c05ebadc | |||
| a8918d55fb | |||
| 07e3ddbe40 | |||
| a9957b45e5 | |||
| 4bd103e612 | |||
| 466234bad6 | |||
| 97beaec25a | |||
| 2b3b638943 | |||
| e1629e8383 | |||
| 266cb614a8 | |||
| dfc4a0818c | |||
| 236ff577a6 | |||
| 66a32ac45d | |||
| 8b2c35b7a3 | |||
| f8f42565ae | |||
| 96981c3c1d | |||
| bd8d207a3d | |||
| e3741ae8fa | |||
| ef87f65bef | |||
| b3537a9acb | |||
| d3d9f23c3c | |||
| c474b8c92e | |||
| da81ba00fb | |||
| f0669ce3ba | |||
| 28c94efaa9 | |||
| 0633954e24 | |||
| cedf09168f | |||
| 3373225f53 | |||
| 1eb9dc73fa | |||
| fdcdc3aaa0 | |||
| da926805b3 | |||
| 82893ffc22 | |||
| beafd9c700 | |||
| c5bd2a7487 | |||
| b4d57f12fe | |||
| 118086aff5 | |||
| 452827630b | |||
| 86248328cc | |||
| 8833d623f7 | |||
| 3fff4609a0 | |||
| 98a1573e27 | |||
| 7080490c68 | |||
| e2b5b92a0c | |||
| 1c3e07afd3 | |||
| c54fa7d266 | |||
| a25f763796 | |||
| 91ad3d729f | |||
| f51ad2cb7e | |||
| 4cb0488839 | |||
| 663229ecde | |||
| cdb623cb66 | |||
| 28d7e0e489 | |||
| aba594b72a | |||
| 5d7c6c3e4b | |||
| 820744dc27 | |||
| 5445f0812f | |||
| 49868fc119 | |||
| a68c784753 | |||
| f2b2f9f959 | |||
| 53dd53553a | |||
| 89b7eac789 | |||
| 9cdba1a50e | |||
| 98b7003504 | |||
| cb2dc6304f | |||
| edb6c0db2e | |||
| a46a055246 | |||
| 9a8f8a051c | |||
| 3eb40ee7d3 | |||
| 4cc453c640 | |||
| 74e27ca94f | |||
| 7dfd7dbcdc | |||
| 2cba1dcb31 | |||
| 48a6a12648 | |||
| 71dd45cd4b | |||
| 6bbf7cbe32 | |||
| 9f36b73081 | |||
| 6ebe84e096 | |||
| a7e485992e | |||
| cb011c8bb0 | |||
| c8875b29c1 | |||
| 978616833a | |||
| ab74ccfe7c | |||
| 92b2013cf6 | |||
| 2e97fb7be2 | |||
| df1934f87b | |||
| d3472feb5c | |||
| addb6db1c7 | |||
| 566d590453 | |||
| e57bd98da0 | |||
| f5c4373f99 | |||
| e762291bb7 | |||
| 9da027d377 | |||
| 96dccdc38b | |||
| d6254294b0 | |||
| 339c1bb573 | |||
| 9782795761 | |||
| 2d2b238da9 | |||
| f0839e02eb | |||
| a9f7d825c8 | |||
| 709b654ed3 | |||
| c265595536 | |||
| 1670aebad8 | |||
| 39460eb100 | |||
| cd659ac2a1 | |||
| ed182d4b3f | |||
| 90614612e2 | |||
| 6b6d4b86a6 | |||
| 0b67473bd3 | |||
| d87ce960d2 | |||
| 69b034349a | |||
| ed00686804 | |||
| e10c469749 | |||
| 5f7e10733a | |||
| d81a214c74 | |||
| e57f240e70 | |||
| e18296e760 | |||
| c145355ccc | |||
| 352a3c39d7 | |||
| c85a4d4fc1 | |||
| 7676641c9e | |||
| 260d2e499e | |||
| a46617f3f0 | |||
| d164b5e7f5 | |||
| c0bf86d11e | |||
| 6f06683d70 | |||
| e3409e52d0 | |||
| 41d6f1b5da | |||
| aca4601734 | |||
| 2a00fcdf5d | |||
| 9200a67a60 | |||
| 80fe5b8140 | |||
| 085e839bfe | |||
| 7ff7e8852b | |||
| 637d714ab2 | |||
| c42d00f048 | |||
| 702e558d1a | |||
| bcc2e8f759 | |||
| 27339715b7 | |||
| 5e2e6411e5 | |||
| 9848740e18 | |||
| 51b150b829 | |||
| 0fac16e769 | |||
| 38ce593c60 | |||
| 798eb22d9a | |||
| 9c108862ef | |||
| 46a45b4596 | |||
| 1205573bc6 | |||
| 9b1c23d49f | |||
| 42ab65abdb | |||
| bb982e6b9b | |||
| 56b1ea3fa5 | |||
| c199b51645 | |||
| 8d2a368da9 | |||
| 052d09e892 | |||
| 439a7dc5e6 | |||
| 4e2e790864 | |||
| 4f3b40bd7b | |||
| b8e1b8d7ac | |||
| fe46e1da53 | |||
| d1869c18ba | |||
| 02cd79a6d9 | |||
| 022ac95bbd | |||
| b3e25cfc52 | |||
| 3d25aa1b91 | |||
| b8c46d6b26 | |||
| 79453ac2c3 | |||
| b08890bd9e | |||
| 938c92fda2 | |||
| 9a9fe83f9c | |||
| bf5d9917ab | |||
| 4f15d681b0 | |||
| c8c6a2ce08 | |||
| a4c040e101 | |||
| 7046c1f546 | |||
| a4ab8e1b06 | |||
| a26559fd3d | |||
| 5448fc6077 | |||
| 06fafbd8eb | |||
| 10156f0067 | |||
| 5632d83afb | |||
| a32473d570 | |||
| e87bb4376f | |||
| 0722cf7ded | |||
| 37fde8f3e8 | |||
| 8655c5b73c | |||
| 31c1450edc | |||
| cbb4207bab | |||
| f5ae50c3ce | |||
| 5efcd14f40 | |||
| 8051e56d13 | |||
| 239987af0b | |||
| 8363ba5a41 | |||
| 30f7127a92 | |||
| 14193abe03 | |||
| 3caf08e199 | |||
| 5cbd38a3e8 | |||
| bbd7ed0f27 | |||
| 43504d16b6 | |||
| 7e8206dcbc | |||
| 3963b807eb | |||
| e99adffc56 | |||
| c9f494d865 | |||
| e8254735bb | |||
| 3d30007756 | |||
| aeaed50446 | |||
| 3ac0fdf619 | |||
| d1b6b6be85 | |||
| 1081fc69e9 | |||
| 73ef43dd2c | |||
| 181e51ef0e | |||
| 1167fdb910 | |||
| ec9109dd89 | |||
| 55aef18851 | |||
| 356d4360ec | |||
| 0f2aa9569b | |||
| ac7ee376e5 | |||
| ab052ce1ac | |||
| 5988e3d3b4 | |||
| 2f2bf5bf29 | |||
| b000f2297d | |||
| 77463a199a | |||
| e54449f5f7 | |||
| d743570546 | |||
| 1dac69635d | |||
| 49f38aeddb | |||
| 43c05cb504 | |||
| ccc90d547c | |||
| 998a010247 | |||
| e4cd5dc552 | |||
| 6cf0b6f651 | |||
| 794bf86904 | |||
| 9ed690ce45 | |||
| 623bf93368 | |||
| 0cc18a6468 | |||
| 87598e10d1 | |||
| ccec7e725a | |||
| f0e6c5483c | |||
| 2cbd35f6d2 | |||
| 930dabe4d6 | |||
| ff4b7ca171 | |||
| 5129fa6677 | |||
| 5336569c25 | |||
| 483843bd0a | |||
| 04df4a31af | |||
| 077ef0f083 | |||
| 5c4b6cda71 | |||
| 4553408b28 | |||
| 42eb33440c | |||
| 87768198a0 | |||
| c24a87df48 | |||
| 2d63f36201 | |||
| 43ac2d3d72 |
@@ -0,0 +1,37 @@
|
|||||||
|
name: Build and Deploy to beta (warm.strudel.cc)
|
||||||
|
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
# Allow one concurrent deployment
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9.12.2
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
# cache: "pnpm"
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
eval $(ssh-agent -s)
|
||||||
|
echo "$SSH_PRIVATE_KEY" | ssh-add -
|
||||||
|
apt update && apt install -y rsync
|
||||||
|
mkdir ~/.ssh
|
||||||
|
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||||
|
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Deploy
|
name: Build and Deploy to live (strudel.cc)
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@ jobs:
|
|||||||
apt update && apt install -y rsync
|
apt update && apt install -y rsync
|
||||||
mkdir ~/.ssh
|
mkdir ~/.ssh
|
||||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy
|
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Strudel tests
|
name: Strudel tests
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: "pnpm"
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm run format-check
|
- run: pnpm run format-check
|
||||||
- run: pnpm run lint
|
- run: pnpm run lint
|
||||||
|
|||||||
+14
@@ -45,6 +45,20 @@ tidal-drum-machines
|
|||||||
webaudiofontdata
|
webaudiofontdata
|
||||||
src-tauri/target
|
src-tauri/target
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### BEGIN Visual Studio Code ###
|
||||||
|
# Blanket, recursive exclude for .vscode directory and files
|
||||||
|
.vscode/**/*
|
||||||
|
# Unexclude specific files and directories within .vscode
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
### END Visual Studio Code ###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# BEGIN JetBrains -> END JetBrains
|
# BEGIN JetBrains -> END JetBrains
|
||||||
|
|
||||||
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
FROM node:24
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm install pnpm --global
|
||||||
|
|
||||||
|
COPY pnpm-workspace.yaml ./
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
COPY packages/ ./packages/
|
||||||
|
COPY examples/ ./examples/
|
||||||
|
RUN mkdir -p website/public
|
||||||
|
COPY website/package.json ./website/
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 4321
|
||||||
|
|
||||||
|
CMD ["pnpm", "dev"]
|
||||||
@@ -42,6 +42,7 @@ export default [
|
|||||||
'**/hydra.mjs',
|
'**/hydra.mjs',
|
||||||
'**/jsdoc-synonyms.js',
|
'**/jsdoc-synonyms.js',
|
||||||
'packages/hs2js/src/hs2js.mjs',
|
'packages/hs2js/src/hs2js.mjs',
|
||||||
|
'packages/supradough/dough-export.mjs',
|
||||||
'**/samples',
|
'**/samples',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import jsdoc from '../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
import { autocompletion } from '@codemirror/autocomplete';
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
import { h } from './html';
|
import { h } from './html';
|
||||||
|
import { Scale } from '@tonaljs/tonal';
|
||||||
|
import { soundMap } from 'superdough';
|
||||||
|
import { complex } from '@strudel/tonal';
|
||||||
|
|
||||||
const escapeHtml = (str) => {
|
const escapeHtml = (str) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@@ -54,11 +57,12 @@ const buildExamples = (examples) =>
|
|||||||
`
|
`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
export const Autocomplete = ({ doc, label }) =>
|
export const Autocomplete = (doc) =>
|
||||||
h`
|
h`
|
||||||
<div class="autocomplete-info-container">
|
<div class="autocomplete-info-container">
|
||||||
<div class="autocomplete-info-tooltip">
|
<div class="autocomplete-info-tooltip">
|
||||||
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
|
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
|
||||||
|
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
|
||||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||||
${buildParamsList(doc.params)}
|
${buildParamsList(doc.params)}
|
||||||
${buildExamples(doc.examples)}
|
${buildExamples(doc.examples)}
|
||||||
@@ -74,29 +78,382 @@ const isValidDoc = (doc) => {
|
|||||||
const hasExcludedTags = (doc) =>
|
const hasExcludedTags = (doc) =>
|
||||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||||
|
|
||||||
const jsdocCompletions = jsdoc.docs
|
export function bankCompletions() {
|
||||||
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
|
const soundDict = soundMap.get();
|
||||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
const banks = new Set();
|
||||||
.map((doc) => ({
|
for (const key of Object.keys(soundDict)) {
|
||||||
label: getDocLabel(doc),
|
const [bank, suffix] = key.split('_');
|
||||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
if (suffix && bank) banks.add(bank);
|
||||||
info: () => Autocomplete({ doc }),
|
}
|
||||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
return Array.from(banks)
|
||||||
}));
|
.sort()
|
||||||
|
.map((name) => ({ label: name, type: 'bank' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to get all scale names from Tonal
|
||||||
|
let scaleCompletions = [];
|
||||||
|
try {
|
||||||
|
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid mode values for voicing
|
||||||
|
const modeCompletions = [
|
||||||
|
{ label: 'below', type: 'mode' },
|
||||||
|
{ label: 'above', type: 'mode' },
|
||||||
|
{ label: 'duck', type: 'mode' },
|
||||||
|
{ label: 'root', type: 'mode' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Valid chord symbols from ireal dictionary plus empty string for major triads
|
||||||
|
const chordSymbols = ['', ...Object.keys(complex)].sort();
|
||||||
|
const chordSymbolCompletions = chordSymbols.map((symbol) => {
|
||||||
|
if (symbol === '') {
|
||||||
|
return {
|
||||||
|
label: 'major',
|
||||||
|
apply: '',
|
||||||
|
type: 'chord-symbol',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: symbol,
|
||||||
|
apply: symbol,
|
||||||
|
type: 'chord-symbol',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getSynonymDoc = (doc, synonym) => {
|
||||||
|
const synonyms = doc.synonyms || [];
|
||||||
|
const docLabel = getDocLabel(doc);
|
||||||
|
// Swap `doc.name` in for `s` in the list of synonyms
|
||||||
|
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
name: synonym,
|
||||||
|
longname: synonym,
|
||||||
|
synonyms: synonymsWithDoc,
|
||||||
|
synonyms_text: synonymsWithDoc.join(', '),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsdocCompletions = (() => {
|
||||||
|
const seen = new Set(); // avoid repetition
|
||||||
|
const completions = [];
|
||||||
|
for (const doc of jsdoc.docs) {
|
||||||
|
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
|
||||||
|
const docLabel = getDocLabel(doc);
|
||||||
|
// Remove duplicates
|
||||||
|
const synonyms = doc.synonyms || [];
|
||||||
|
let labels = [docLabel, ...synonyms];
|
||||||
|
for (const label of labels) {
|
||||||
|
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||||
|
if (label && !seen.has(label)) {
|
||||||
|
seen.add(label);
|
||||||
|
completions.push({
|
||||||
|
label,
|
||||||
|
info: () => Autocomplete(getSynonymDoc(doc, label)),
|
||||||
|
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return completions;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// --- Handler functions for each context ---
|
||||||
|
const pitchNames = [
|
||||||
|
'C',
|
||||||
|
'C#',
|
||||||
|
'Db',
|
||||||
|
'D',
|
||||||
|
'D#',
|
||||||
|
'Eb',
|
||||||
|
'E',
|
||||||
|
'E#',
|
||||||
|
'Fb',
|
||||||
|
'F',
|
||||||
|
'F#',
|
||||||
|
'Gb',
|
||||||
|
'G',
|
||||||
|
'G#',
|
||||||
|
'Ab',
|
||||||
|
'A',
|
||||||
|
'A#',
|
||||||
|
'Bb',
|
||||||
|
'B',
|
||||||
|
'B#',
|
||||||
|
'Cb',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Cached regex patterns for scaleHandler
|
||||||
|
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
|
||||||
|
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
|
||||||
|
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
|
||||||
|
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
|
||||||
|
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
|
||||||
|
|
||||||
|
function scaleHandler(context) {
|
||||||
|
// First check for scale context without quotes - block with empty completions
|
||||||
|
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
|
||||||
|
if (scaleNoQuotesContext) {
|
||||||
|
return {
|
||||||
|
from: scaleNoQuotesContext.to,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for after-colon context first (more specific)
|
||||||
|
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
|
||||||
|
if (scaleAfterColonContext) {
|
||||||
|
const text = scaleAfterColonContext.text;
|
||||||
|
const colonIdx = text.lastIndexOf(':');
|
||||||
|
if (colonIdx !== -1) {
|
||||||
|
const fragment = text.slice(colonIdx + 1);
|
||||||
|
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
|
||||||
|
const options = filteredScales.map((s) => ({
|
||||||
|
...s,
|
||||||
|
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
|
||||||
|
}));
|
||||||
|
const from = scaleAfterColonContext.from + colonIdx + 1;
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for pre-colon context
|
||||||
|
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
|
||||||
|
if (scalePreColonContext) {
|
||||||
|
if (!scalePreColonContext.text.includes(':')) {
|
||||||
|
if (context.explicit) {
|
||||||
|
const text = scalePreColonContext.text;
|
||||||
|
const match = text.match(SCALE_PITCH_MATCH_REGEX);
|
||||||
|
const fragment = match ? match[0] : '';
|
||||||
|
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||||
|
const from = scalePreColonContext.to - fragment.length;
|
||||||
|
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
||||||
|
return { from, options };
|
||||||
|
} else {
|
||||||
|
return { from: scalePreColonContext.to, options: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached regex patterns for soundHandler
|
||||||
|
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
|
||||||
|
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
|
||||||
|
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
|
||||||
|
|
||||||
|
function soundHandler(context) {
|
||||||
|
// First check for sound context without quotes - block with empty completions
|
||||||
|
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
|
||||||
|
if (soundNoQuotesContext) {
|
||||||
|
return {
|
||||||
|
from: soundNoQuotesContext.to,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for sound context with quotes - provide completions
|
||||||
|
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
|
||||||
|
if (!soundContext) return null;
|
||||||
|
|
||||||
|
const text = soundContext.text;
|
||||||
|
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||||
|
if (quoteIdx === -1) return null;
|
||||||
|
const inside = text.slice(quoteIdx + 1);
|
||||||
|
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
|
||||||
|
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||||
|
const soundNames = Object.keys(soundMap.get()).sort();
|
||||||
|
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
|
||||||
|
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
|
||||||
|
const from = soundContext.to - fragment.length;
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached regex patterns for bankHandler
|
||||||
|
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
|
||||||
|
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
|
||||||
|
|
||||||
|
function bankHandler(context) {
|
||||||
|
// First check for bank context without quotes - block with empty completions
|
||||||
|
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
|
||||||
|
if (bankNoQuotesContext) {
|
||||||
|
return {
|
||||||
|
from: bankNoQuotesContext.to,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for bank context with quotes - provide completions
|
||||||
|
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
|
||||||
|
if (!bankMatch) return null;
|
||||||
|
|
||||||
|
const text = bankMatch.text;
|
||||||
|
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||||
|
if (quoteIdx === -1) return null;
|
||||||
|
const inside = text.slice(quoteIdx + 1);
|
||||||
|
const fragment = inside;
|
||||||
|
let banks = bankCompletions();
|
||||||
|
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
|
||||||
|
const from = bankMatch.to - fragment.length;
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
options: filteredBanks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached regex patterns for modeHandler
|
||||||
|
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
|
||||||
|
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
|
||||||
|
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
|
||||||
|
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
|
||||||
|
|
||||||
|
function modeHandler(context) {
|
||||||
|
// First check for mode context without quotes - block with empty completions
|
||||||
|
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
|
||||||
|
if (modeNoQuotesContext) {
|
||||||
|
return {
|
||||||
|
from: modeNoQuotesContext.to,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for after-colon context first (more specific)
|
||||||
|
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
|
||||||
|
if (modeAfterColonContext) {
|
||||||
|
const text = modeAfterColonContext.text;
|
||||||
|
const colonIdx = text.lastIndexOf(':');
|
||||||
|
if (colonIdx !== -1) {
|
||||||
|
const fragment = text.slice(colonIdx + 1);
|
||||||
|
// For anchor after colon, we can suggest pitch names
|
||||||
|
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||||
|
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
||||||
|
const from = modeAfterColonContext.from + colonIdx + 1;
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for pre-colon context
|
||||||
|
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
|
||||||
|
if (!modeContext) return null;
|
||||||
|
|
||||||
|
const text = modeContext.text;
|
||||||
|
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||||
|
if (quoteIdx === -1) return null;
|
||||||
|
const inside = text.slice(quoteIdx + 1);
|
||||||
|
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
|
||||||
|
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||||
|
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
|
||||||
|
const from = modeContext.to - fragment.length;
|
||||||
|
return {
|
||||||
|
from,
|
||||||
|
options: filteredModes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached regex patterns for chordHandler
|
||||||
|
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
|
||||||
|
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
|
||||||
|
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
|
||||||
|
|
||||||
|
function chordHandler(context) {
|
||||||
|
// First check for chord context without quotes - block with empty completions
|
||||||
|
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
|
||||||
|
if (chordNoQuotesContext) {
|
||||||
|
return {
|
||||||
|
from: chordNoQuotesContext.to,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for chord context with quotes - provide completions
|
||||||
|
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
|
||||||
|
if (!chordContext) return null;
|
||||||
|
|
||||||
|
const text = chordContext.text;
|
||||||
|
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||||
|
if (quoteIdx === -1) return null;
|
||||||
|
const inside = text.slice(quoteIdx + 1);
|
||||||
|
|
||||||
|
// Use same fragment matching as sound/mode for expressions like "<G Am>"
|
||||||
|
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
|
||||||
|
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||||
|
|
||||||
|
// Check if fragment contains any pitch name at start (for root + symbol)
|
||||||
|
let rootMatch = null;
|
||||||
|
let symbolFragment = fragment;
|
||||||
|
for (const pitch of pitchNames) {
|
||||||
|
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
|
||||||
|
rootMatch = pitch;
|
||||||
|
symbolFragment = fragment.slice(pitch.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootMatch) {
|
||||||
|
// We have a root, now complete chord symbols
|
||||||
|
const filteredSymbols = chordSymbolCompletions.filter((s) =>
|
||||||
|
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create completions that replace the entire chord, not just the symbol part
|
||||||
|
const options = filteredSymbols;
|
||||||
|
|
||||||
|
const from = chordContext.to - symbolFragment.length;
|
||||||
|
return { from, options };
|
||||||
|
} else {
|
||||||
|
// No root yet, complete with pitch names
|
||||||
|
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||||
|
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
|
||||||
|
const from = chordContext.to - fragment.length;
|
||||||
|
return { from, options };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached regex patterns for fallbackHandler
|
||||||
|
const FALLBACK_WORD_REGEX = /\w*/;
|
||||||
|
|
||||||
|
function fallbackHandler(context) {
|
||||||
|
const word = context.matchBefore(FALLBACK_WORD_REGEX);
|
||||||
|
if (word && word.from === word.to && !context.explicit) return null;
|
||||||
|
if (word) {
|
||||||
|
return {
|
||||||
|
from: word.from,
|
||||||
|
options: jsdocCompletions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = [
|
||||||
|
soundHandler,
|
||||||
|
bankHandler,
|
||||||
|
chordHandler,
|
||||||
|
scaleHandler,
|
||||||
|
modeHandler,
|
||||||
|
// this handler *must* be last
|
||||||
|
fallbackHandler,
|
||||||
|
];
|
||||||
|
|
||||||
export const strudelAutocomplete = (context) => {
|
export const strudelAutocomplete = (context) => {
|
||||||
const word = context.matchBefore(/\w*/);
|
for (const handler of handlers) {
|
||||||
if (word.from === word.to && !context.explicit) return null;
|
const result = handler(context);
|
||||||
|
if (result) {
|
||||||
return {
|
return result;
|
||||||
from: word.from,
|
}
|
||||||
options: jsdocCompletions,
|
}
|
||||||
/* options: [
|
return null;
|
||||||
{ label: 'match', type: 'keyword' },
|
|
||||||
{ label: 'hello', type: 'variable', info: '(World)' },
|
|
||||||
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
|
||||||
], */
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAutoCompletionEnabled = (enabled) =>
|
export const isAutoCompletionEnabled = (enabled) =>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
keymap,
|
||||||
|
highlightSpecialChars,
|
||||||
|
drawSelection,
|
||||||
|
highlightActiveLine,
|
||||||
|
dropCursor,
|
||||||
|
rectangularSelection,
|
||||||
|
crosshairCursor,
|
||||||
|
lineNumbers,
|
||||||
|
highlightActiveLineGutter,
|
||||||
|
} from '@codemirror/view';
|
||||||
|
import {
|
||||||
|
defaultHighlightStyle,
|
||||||
|
syntaxHighlighting,
|
||||||
|
bracketMatching,
|
||||||
|
foldGutter,
|
||||||
|
foldKeymap,
|
||||||
|
} from '@codemirror/language';
|
||||||
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
|
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||||
|
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||||
|
|
||||||
|
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||||
|
|
||||||
|
export const basicSetup = (() => [
|
||||||
|
// lineNumbers(),
|
||||||
|
// highlightActiveLineGutter(),
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
// foldGutter(),
|
||||||
|
// drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
|
// EditorState.allowMultipleSelections.of(true),
|
||||||
|
// indentOnInput(),
|
||||||
|
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
// autocompletion(),
|
||||||
|
rectangularSelection(),
|
||||||
|
crosshairCursor(),
|
||||||
|
// highlightActiveLine(),
|
||||||
|
// highlightSelectionMatches(),
|
||||||
|
keymap.of([
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
// ...searchKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
// ...foldKeymap,
|
||||||
|
// ...completionKeymap,
|
||||||
|
]),
|
||||||
|
])();
|
||||||
|
|
||||||
|
/// A minimal set of extensions to create a functional editor. Only
|
||||||
|
/// includes [the default keymap](#commands.defaultKeymap), [undo
|
||||||
|
/// history](#commands.history), [special character
|
||||||
|
/// highlighting](#view.highlightSpecialChars), [custom selection
|
||||||
|
/// drawing](#view.drawSelection), and [default highlight
|
||||||
|
/// style](#language.defaultHighlightStyle).
|
||||||
|
export const minimalSetup = (() => [
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
drawSelection(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||||
|
])();
|
||||||
@@ -1,29 +1,31 @@
|
|||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
import { indentWithTab, toggleLineComment } from '@codemirror/commands';
|
||||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
import { history, indentWithTab } from '@codemirror/commands';
|
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
|
||||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
import {
|
import {
|
||||||
|
drawSelection,
|
||||||
EditorView,
|
EditorView,
|
||||||
highlightActiveLineGutter,
|
|
||||||
highlightActiveLine,
|
highlightActiveLine,
|
||||||
|
highlightActiveLineGutter,
|
||||||
keymap,
|
keymap,
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
drawSelection,
|
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { repl, registerControl } from '@strudel/core';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { Drawer, cleanupDraw } from '@strudel/draw';
|
import { logger, registerControl, repl } from '@strudel/core';
|
||||||
|
import { cleanupDraw, Drawer } from '@strudel/draw';
|
||||||
|
|
||||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { basicSetup } from './basicSetup.mjs';
|
||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.mjs';
|
import { keybindings } from './keybindings.mjs';
|
||||||
import { initTheme, activateTheme, theme } from './themes.mjs';
|
|
||||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
|
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||||
|
|
||||||
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
const extensions = {
|
const extensions = {
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
@@ -85,13 +87,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
/* search(),
|
/* search(),
|
||||||
highlightSelectionMatches(), */
|
highlightSelectionMatches(), */
|
||||||
...initialSettings,
|
...initialSettings,
|
||||||
|
basicSetup,
|
||||||
mondo ? [] : javascript(),
|
mondo ? [] : javascript(),
|
||||||
|
javascriptLanguage.data.of({
|
||||||
|
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||||
|
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||||
|
}),
|
||||||
sliderPlugin,
|
sliderPlugin,
|
||||||
widgetPlugin,
|
widgetPlugin,
|
||||||
// indentOnInput(), // works without. already brought with javascript extension?
|
// indentOnInput(), // works without. already brought with javascript
|
||||||
// bracketMatching(), // does not do anything
|
// extension? bracketMatching(), // does not do anything
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
history(),
|
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
drawSelection({ cursorBlinkRate: 0 }),
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
Prec.highest(
|
Prec.highest(
|
||||||
@@ -114,13 +120,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
run: () => onStop?.(),
|
run: () => onStop?.(),
|
||||||
},
|
},
|
||||||
/* {
|
/* {
|
||||||
key: 'Ctrl-Shift-.',
|
key: 'Ctrl-Shift-.',
|
||||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Ctrl-Shift-Enter',
|
key: 'Ctrl-Shift-Enter',
|
||||||
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
|
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
|
||||||
}, */
|
}, */
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -201,7 +207,8 @@ export class StrudelMirror {
|
|||||||
updateWidgets(this.editor, widgets);
|
updateWidgets(this.editor, widgets);
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
updateMiniLocations(this.editor, this.miniLocations);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
// if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
|
// if no painters are set (.onPaint was not called), then we only need
|
||||||
|
// the present moment (for highlighting)
|
||||||
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
|
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
|
||||||
this.drawer.setDrawTime(drawTime);
|
this.drawer.setDrawTime(drawTime);
|
||||||
// invalidate drawer after we've set the appropriate drawTime
|
// invalidate drawer after we've set the appropriate drawTime
|
||||||
@@ -240,6 +247,33 @@ export class StrudelMirror {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('start-repl', this.onStartRepl);
|
document.addEventListener('start-repl', this.onStartRepl);
|
||||||
|
|
||||||
|
// Handle global evaluation requests (e.g., from Vim :w)
|
||||||
|
this.onEvaluateRequest = (e) => {
|
||||||
|
try {
|
||||||
|
// Evaluate current editor on repl-evaluate
|
||||||
|
logger('[repl] evaluate via event');
|
||||||
|
this.evaluate();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-evaluate event', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('repl-evaluate', this.onEvaluateRequest);
|
||||||
|
document.addEventListener('repl-stop', this.onStopRequest);
|
||||||
|
|
||||||
|
// Toggle comments requested from Vim (gc)
|
||||||
|
this.onToggleComment = (e) => {
|
||||||
|
try {
|
||||||
|
// Honor selections; toggleLineComment handles both selections and
|
||||||
|
// single line
|
||||||
|
toggleLineComment(this.editor);
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-toggle-comment event', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('repl-toggle-comment', this.onToggleComment);
|
||||||
}
|
}
|
||||||
draw(haps, time, painters) {
|
draw(haps, time, painters) {
|
||||||
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
||||||
@@ -266,6 +300,16 @@ export class StrudelMirror {
|
|||||||
async stop() {
|
async stop() {
|
||||||
this.repl.scheduler.stop();
|
this.repl.scheduler.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for global stop requests (e.g., from Vim :q)
|
||||||
|
onStopRequest = (e) => {
|
||||||
|
try {
|
||||||
|
this.stop();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-stop event', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
async toggle() {
|
async toggle() {
|
||||||
if (this.repl.scheduler.started) {
|
if (this.repl.scheduler.started) {
|
||||||
this.repl.stop();
|
this.repl.stop();
|
||||||
@@ -341,11 +385,18 @@ export class StrudelMirror {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCode(code) {
|
setCode(code) {
|
||||||
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
|
const changes = {
|
||||||
|
from: 0,
|
||||||
|
to: this.editor.state.doc.length,
|
||||||
|
insert: code,
|
||||||
|
};
|
||||||
this.editor.dispatch({ changes });
|
this.editor.dispatch({ changes });
|
||||||
}
|
}
|
||||||
clear() {
|
clear() {
|
||||||
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||||
|
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
||||||
|
this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest);
|
||||||
|
this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment);
|
||||||
}
|
}
|
||||||
getCursorLocation() {
|
getCursorLocation() {
|
||||||
return this.editor.state.selection.main.head;
|
return this.editor.state.selection.main.head;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
|
||||||
export let html = (string) => {
|
export let html = (string) => {
|
||||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = string.trim();
|
||||||
|
return template.content.childNodes;
|
||||||
};
|
};
|
||||||
let parseChunk = (chunk) => {
|
let parseChunk = (chunk) => {
|
||||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
import { Prec } from '@codemirror/state';
|
import { Prec } from '@codemirror/state';
|
||||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||||
// import { searchKeymap } from '@codemirror/search';
|
// import { searchKeymap } from '@codemirror/search';
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
import { vim } from '@replit/codemirror-vim';
|
import { vim, Vim } from '@replit/codemirror-vim';
|
||||||
|
// import { vim } from './vim_test.mjs';
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
@@ -18,6 +20,110 @@ const vscodePlugin = ViewPlugin.fromClass(
|
|||||||
);
|
);
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||||
|
|
||||||
|
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
|
||||||
|
// event 'repl-evaluate' that the editor listens for, and also simulate
|
||||||
|
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
||||||
|
// appears in the Console panel.
|
||||||
|
try {
|
||||||
|
if (Vim && typeof Vim.defineEx === 'function') {
|
||||||
|
// Map gc to toggle line comments by dispatching a custom event that our
|
||||||
|
// CodeMirror integration listens to. This avoids depending on Vim's
|
||||||
|
// internal actions and works with current selections/visual mode.
|
||||||
|
try {
|
||||||
|
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||||
|
const view = cm?.view || cm;
|
||||||
|
try {
|
||||||
|
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
|
document.dispatchEvent(ev);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('strudelToggleComment dispatch failed', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
|
||||||
|
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Vim gc mapping failed', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :q to pause/stop
|
||||||
|
Vim.defineEx('quit', 'q', (cm) => {
|
||||||
|
try {
|
||||||
|
const view = cm?.view || cm;
|
||||||
|
// First try dispatching our custom stop event, then fallback to Alt+.
|
||||||
|
let handled = false;
|
||||||
|
try {
|
||||||
|
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
|
handled = document.dispatchEvent(ev) === false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching repl-stop event', e);
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
const altDot = new KeyboardEvent('keydown', {
|
||||||
|
key: '.',
|
||||||
|
code: 'Period',
|
||||||
|
altKey: true,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
view?.dom?.dispatchEvent?.(altDot);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching :q stop event', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// :w to evaluate
|
||||||
|
Vim.defineEx('write', 'w', (cm) => {
|
||||||
|
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
|
||||||
|
try {
|
||||||
|
view?.focus?.();
|
||||||
|
// Let the app know this came from Vim :w
|
||||||
|
try {
|
||||||
|
logger('[vim] :w — evaluating code');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error logging Vim :w evaluation', e);
|
||||||
|
}
|
||||||
|
// Dispatch a dedicated evaluate event first
|
||||||
|
let handled = false;
|
||||||
|
try {
|
||||||
|
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
|
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching repl-evaluate event', e);
|
||||||
|
}
|
||||||
|
if (handled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Try Ctrl+Enter first if not handled by custom event
|
||||||
|
const ctrlEnter = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
ctrlKey: true,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
view?.dom?.dispatchEvent?.(ctrlEnter);
|
||||||
|
// If not handled (no handler called preventDefault), try Alt+Enter as
|
||||||
|
// fallback
|
||||||
|
if (!ctrlEnter.defaultPrevented) {
|
||||||
|
const altEnter = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
altKey: true,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
view?.dom?.dispatchEvent?.(altEnter);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching :w evaluation event', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
||||||
|
}
|
||||||
|
|
||||||
const keymaps = {
|
const keymaps = {
|
||||||
vim,
|
vim,
|
||||||
emacs,
|
emacs,
|
||||||
@@ -27,5 +133,5 @@ const keymaps = {
|
|||||||
|
|
||||||
export function keybindings(name) {
|
export function keybindings(name) {
|
||||||
const active = keymaps[name];
|
const active = keymaps[name];
|
||||||
return [active ? active() : [], keymap.of(historyKeymap)];
|
return [active ? Prec.high(active()) : []];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "1.2.4",
|
"version": "1.2.6",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -42,12 +42,15 @@
|
|||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
"@replit/codemirror-vim": "^6.2.1",
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"nanostores": "^0.11.3"
|
"@tonaljs/tonal": "^4.10.0",
|
||||||
|
"nanostores": "^0.11.3",
|
||||||
|
"superdough": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
import jsdoc from '../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
import { Autocomplete } from './autocomplete.mjs';
|
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
|
|
||||||
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
|
|||||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
// Try for synonyms
|
// Try for synonyms
|
||||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||||
if (!entry) {
|
if (!doc) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
entry = getSynonymDoc(doc, word);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
|
|||||||
create(view) {
|
create(view) {
|
||||||
let dom = document.createElement('div');
|
let dom = document.createElement('div');
|
||||||
dom.className = 'strudel-tooltip';
|
dom.className = 'strudel-tooltip';
|
||||||
const ac = Autocomplete({ doc: entry, label: word });
|
const ac = Autocomplete(entry);
|
||||||
dom.appendChild(ac);
|
dom.appendChild(ac);
|
||||||
return { dom };
|
return { dom };
|
||||||
},
|
},
|
||||||
|
|||||||
+397
-34
@@ -87,10 +87,244 @@ export function registerControl(names, ...aliases) {
|
|||||||
*/
|
*/
|
||||||
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position in the wavetable of the wavetable oscillator
|
||||||
|
*
|
||||||
|
* @name wt
|
||||||
|
* @param {number | Pattern} position Position in the wavetable from 0 to 1
|
||||||
|
* @synonyms wavetablePosition
|
||||||
|
* @example
|
||||||
|
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
|
||||||
|
*/
|
||||||
|
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of envelope applied wavetable oscillator's position envelope
|
||||||
|
*
|
||||||
|
* @name wtenv
|
||||||
|
* @param {number | Pattern} amount between 0 and 1
|
||||||
|
*/
|
||||||
|
export const { wtenv } = registerControl('wtenv');
|
||||||
|
/**
|
||||||
|
* Attack time of the wavetable oscillator's position envelope
|
||||||
|
*
|
||||||
|
* @name wtattack
|
||||||
|
* @synonyms wtatt
|
||||||
|
* @param {number | Pattern} time attack time in seconds
|
||||||
|
*/
|
||||||
|
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decay time of the wavetable oscillator's position envelope
|
||||||
|
*
|
||||||
|
* @name wtdecay
|
||||||
|
* @synonyms wtdec
|
||||||
|
* @param {number | Pattern} time decay time in seconds
|
||||||
|
*/
|
||||||
|
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sustain time of the wavetable oscillator's position envelope
|
||||||
|
*
|
||||||
|
* @name wtsustain
|
||||||
|
* @synonyms wtsus
|
||||||
|
* @param {number | Pattern} gain sustain level (0 to 1)
|
||||||
|
*/
|
||||||
|
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release time of the wavetable oscillator's position envelope
|
||||||
|
*
|
||||||
|
* @name wtrelease
|
||||||
|
* @synonyms wtrel
|
||||||
|
* @param {number | Pattern} time release time in seconds
|
||||||
|
*/
|
||||||
|
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate of the LFO for the wavetable oscillator's position
|
||||||
|
*
|
||||||
|
* @name wtrate
|
||||||
|
* @param {number | Pattern} rate rate in hertz
|
||||||
|
*/
|
||||||
|
export const { wtrate } = registerControl('wtrate');
|
||||||
|
/**
|
||||||
|
* cycle synced rate of the LFO for the wavetable oscillator's position
|
||||||
|
*
|
||||||
|
* @name wtsync
|
||||||
|
* @param {number | Pattern} rate rate in cycles
|
||||||
|
*/
|
||||||
|
export const { wtsync } = registerControl('wtsync');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depth of the LFO for the wavetable oscillator's position
|
||||||
|
*
|
||||||
|
* @name wtdepth
|
||||||
|
* @param {number | Pattern} depth depth of modulation
|
||||||
|
*/
|
||||||
|
export const { wtdepth } = registerControl('wtdepth');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of the LFO for the wavetable oscillator's position
|
||||||
|
*
|
||||||
|
* @name wtshape
|
||||||
|
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||||
|
*/
|
||||||
|
export const { wtshape } = registerControl('wtshape');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DC offset of the LFO for the wavetable oscillator's position
|
||||||
|
*
|
||||||
|
* @name wtdc
|
||||||
|
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||||
|
*/
|
||||||
|
export const { wtdc } = registerControl('wtdc');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skew of the LFO for the wavetable oscillator's position
|
||||||
|
*
|
||||||
|
* @name wtskew
|
||||||
|
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||||
|
*/
|
||||||
|
export const { wtskew } = registerControl('wtskew');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
|
||||||
|
*
|
||||||
|
* @name warp
|
||||||
|
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
|
||||||
|
* @synonyms wavetableWarp
|
||||||
|
* @example
|
||||||
|
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
|
||||||
|
* .warpmode("spin")
|
||||||
|
*/
|
||||||
|
export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attack time of the wavetable oscillator's warp envelope
|
||||||
|
*
|
||||||
|
* @name warpattack
|
||||||
|
* @synonyms warpatt
|
||||||
|
* @param {number | Pattern} time attack time in seconds
|
||||||
|
*/
|
||||||
|
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decay time of the wavetable oscillator's warp envelope
|
||||||
|
*
|
||||||
|
* @name warpdecay
|
||||||
|
* @synonyms warpdec
|
||||||
|
* @param {number | Pattern} time decay time in seconds
|
||||||
|
*/
|
||||||
|
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sustain time of the wavetable oscillator's warp envelope
|
||||||
|
*
|
||||||
|
* @name warpsustain
|
||||||
|
* @synonyms warpsus
|
||||||
|
* @param {number | Pattern} gain sustain level (0 to 1)
|
||||||
|
*/
|
||||||
|
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release time of the wavetable oscillator's warp envelope
|
||||||
|
*
|
||||||
|
* @name warprelease
|
||||||
|
* @synonyms warprel
|
||||||
|
* @param {number | Pattern} time release time in seconds
|
||||||
|
*/
|
||||||
|
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate of the LFO for the wavetable oscillator's warp
|
||||||
|
*
|
||||||
|
* @name warprate
|
||||||
|
* @param {number | Pattern} rate rate in hertz
|
||||||
|
*/
|
||||||
|
export const { warprate } = registerControl('warprate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depth of the LFO for the wavetable oscillator's warp
|
||||||
|
*
|
||||||
|
* @name warpdepth
|
||||||
|
* @param {number | Pattern} depth depth of modulation
|
||||||
|
*/
|
||||||
|
export const { warpdepth } = registerControl('warpdepth');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of the LFO for the wavetable oscillator's warp
|
||||||
|
*
|
||||||
|
* @name warpshape
|
||||||
|
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||||
|
*/
|
||||||
|
export const { warpshape } = registerControl('warpshape');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DC offset of the LFO for the wavetable oscillator's warp
|
||||||
|
*
|
||||||
|
* @name warpdc
|
||||||
|
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||||
|
*/
|
||||||
|
export const { warpdc } = registerControl('warpdc');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skew of the LFO for the wavetable oscillator's warp
|
||||||
|
*
|
||||||
|
* @name warpskew
|
||||||
|
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||||
|
*/
|
||||||
|
export const { warpskew } = registerControl('warpskew');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of warp (alteration of the waveform) to apply to the wavetable oscillator.
|
||||||
|
*
|
||||||
|
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
|
||||||
|
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
|
||||||
|
*
|
||||||
|
* @name warpmode
|
||||||
|
* @param {number | string | Pattern} mode Warp mode
|
||||||
|
* @synonyms wavetableWarpMode
|
||||||
|
* @example
|
||||||
|
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
|
||||||
|
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of randomness of the initial phase of the wavetable oscillator.
|
||||||
|
*
|
||||||
|
* @name wtphaserand
|
||||||
|
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
|
||||||
|
* @synonyms wavetablePhaseRand
|
||||||
|
* @example
|
||||||
|
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of envelope applied wavetable oscillator's position envelope
|
||||||
|
*
|
||||||
|
* @name warpenv
|
||||||
|
* @param {number | Pattern} amount between 0 and 1
|
||||||
|
*/
|
||||||
|
export const { warpenv } = registerControl('warpenv');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cycle synced rate of the LFO for the wavetable warp position
|
||||||
|
*
|
||||||
|
* @name warpsync
|
||||||
|
* @param {number | Pattern} rate rate in cycles
|
||||||
|
*/
|
||||||
|
export const { warpsync } = registerControl('warpsync');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a custom webaudio node to use as a sound source.
|
* Define a custom webaudio node to use as a sound source.
|
||||||
*
|
*
|
||||||
* @name source
|
* @name source
|
||||||
|
* @synonyms src
|
||||||
* @param {function} getSource
|
* @param {function} getSource
|
||||||
* @synonyms src
|
* @synonyms src
|
||||||
*
|
*
|
||||||
@@ -113,7 +347,7 @@ export const { n } = registerControl('n');
|
|||||||
*
|
*
|
||||||
* - a letter (a-g or A-G)
|
* - a letter (a-g or A-G)
|
||||||
* - optional accidentals (b or #)
|
* - optional accidentals (b or #)
|
||||||
* - optional octave number (0-9). Defaults to 3
|
* - optional (possibly negative) octave number (0-9). Defaults to 3
|
||||||
*
|
*
|
||||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||||
*
|
*
|
||||||
@@ -126,6 +360,8 @@ export const { n } = registerControl('n');
|
|||||||
* note("c4 a4 f4 e4")
|
* note("c4 a4 f4 e4")
|
||||||
* @example
|
* @example
|
||||||
* note("60 69 65 64")
|
* note("60 69 65 64")
|
||||||
|
* @example
|
||||||
|
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
|
||||||
*/
|
*/
|
||||||
export const { note } = registerControl(['note', 'n']);
|
export const { note } = registerControl(['note', 'n']);
|
||||||
|
|
||||||
@@ -141,8 +377,8 @@ export const { note } = registerControl(['note', 'n']);
|
|||||||
*/
|
*/
|
||||||
export const { accelerate } = registerControl('accelerate');
|
export const { accelerate } = registerControl('accelerate');
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
||||||
|
*
|
||||||
* @name velocity
|
* @name velocity
|
||||||
* @example
|
* @example
|
||||||
* s("hh*8")
|
* s("hh*8")
|
||||||
@@ -254,7 +490,7 @@ export const { fmenv } = registerControl('fmenv');
|
|||||||
export const { fmattack } = registerControl('fmattack');
|
export const { fmattack } = registerControl('fmattack');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* waveform of the fm modulator
|
* Waveform of the fm modulator
|
||||||
*
|
*
|
||||||
* @name fmwave
|
* @name fmwave
|
||||||
* @param {number | Pattern} wave waveform
|
* @param {number | Pattern} wave waveform
|
||||||
@@ -309,6 +545,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
|
|||||||
*/
|
*/
|
||||||
export const { bank } = registerControl('bank');
|
export const { bank } = registerControl('bank');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mix control for the chorus effect
|
||||||
|
*
|
||||||
|
* @name chorus
|
||||||
|
* @param {string | Pattern} chorus mix amount between 0 and 1
|
||||||
|
* @example
|
||||||
|
* note("d d a# a").s("sawtooth").chorus(.5)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { chorus } = registerControl('chorus');
|
||||||
|
|
||||||
// analyser node send amount 0 - 1 (used by scope)
|
// analyser node send amount 0 - 1 (used by scope)
|
||||||
export const { analyze } = registerControl('analyze');
|
export const { analyze } = registerControl('analyze');
|
||||||
// fftSize of analyser
|
// fftSize of analyser
|
||||||
@@ -377,7 +624,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
|
|||||||
// ['bpq'],
|
// ['bpq'],
|
||||||
export const { bandq, bpq } = registerControl('bandq', 'bpq');
|
export const { bandq, bpq } = registerControl('bandq', 'bpq');
|
||||||
/**
|
/**
|
||||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
* A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||||
*
|
*
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name begin
|
* @name begin
|
||||||
@@ -438,7 +685,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
|
|||||||
*/
|
*/
|
||||||
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||||
/**
|
/**
|
||||||
* bit crusher effect.
|
* Bit crusher effect.
|
||||||
*
|
*
|
||||||
* @name crush
|
* @name crush
|
||||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||||
@@ -449,7 +696,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
|||||||
// ['clhatdecay'],
|
// ['clhatdecay'],
|
||||||
export const { crush } = registerControl('crush');
|
export const { crush } = registerControl('crush');
|
||||||
/**
|
/**
|
||||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||||
*
|
*
|
||||||
* @name coarse
|
* @name coarse
|
||||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||||
@@ -460,7 +707,7 @@ export const { crush } = registerControl('crush');
|
|||||||
export const { coarse } = registerControl('coarse');
|
export const { coarse } = registerControl('coarse');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modulate the amplitude of a sound with a continuous waveform
|
* Modulate the amplitude of a sound with a continuous waveform
|
||||||
*
|
*
|
||||||
* @name tremolo
|
* @name tremolo
|
||||||
* @synonyms trem
|
* @synonyms trem
|
||||||
@@ -472,7 +719,7 @@ export const { coarse } = registerControl('coarse');
|
|||||||
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modulate the amplitude of a sound with a continuous waveform
|
* Modulate the amplitude of a sound with a continuous waveform
|
||||||
*
|
*
|
||||||
* @name tremolosync
|
* @name tremolosync
|
||||||
* @synonyms tremsync
|
* @synonyms tremsync
|
||||||
@@ -487,7 +734,7 @@ export const { tremolosync } = registerControl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* depth of amplitude modulation
|
* Depth of amplitude modulation
|
||||||
*
|
*
|
||||||
* @name tremolodepth
|
* @name tremolodepth
|
||||||
* @synonyms tremdepth
|
* @synonyms tremdepth
|
||||||
@@ -498,7 +745,7 @@ export const { tremolosync } = registerControl(
|
|||||||
*/
|
*/
|
||||||
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
||||||
/**
|
/**
|
||||||
* alter the shape of the modulation waveform
|
* Alter the shape of the modulation waveform
|
||||||
*
|
*
|
||||||
* @name tremoloskew
|
* @name tremoloskew
|
||||||
* @synonyms tremskew
|
* @synonyms tremskew
|
||||||
@@ -510,7 +757,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
|||||||
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* alter the phase of the modulation waveform
|
* Alter the phase of the modulation waveform
|
||||||
*
|
*
|
||||||
* @name tremolophase
|
* @name tremolophase
|
||||||
* @synonyms tremphase
|
* @synonyms tremphase
|
||||||
@@ -522,9 +769,10 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
|||||||
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shape of amplitude modulation
|
* Shape of amplitude modulation
|
||||||
*
|
*
|
||||||
* @name tremoloshape
|
* @name tremoloshape
|
||||||
|
* @synonyms tremshape
|
||||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||||
* @example
|
* @example
|
||||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||||
@@ -532,7 +780,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
|||||||
*/
|
*/
|
||||||
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||||
/**
|
/**
|
||||||
* filter overdrive for supported filter types
|
* Filter overdrive for supported filter types
|
||||||
*
|
*
|
||||||
* @name drive
|
* @name drive
|
||||||
* @param {number | Pattern} amount
|
* @param {number | Pattern} amount
|
||||||
@@ -540,44 +788,94 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
|||||||
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const { drive } = registerControl('drive');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modulate the amplitude of an orbit to create a "sidechain" like effect
|
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
|
||||||
|
*
|
||||||
|
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
|
||||||
*
|
*
|
||||||
* @name duckorbit
|
* @name duckorbit
|
||||||
|
* @synonyms duck
|
||||||
* @param {number | Pattern} orbit target orbit
|
* @param {number | Pattern} orbit target orbit
|
||||||
* @example
|
* @example
|
||||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
* $: 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)
|
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
|
||||||
|
* @example
|
||||||
|
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* $: s("hh*16").orbit(3)
|
||||||
|
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { duck } = registerControl('duckorbit', 'duck');
|
export const { duck } = registerControl('duckorbit', 'duck');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the amount of ducking applied to target orbit
|
* The amount of ducking applied to target orbit
|
||||||
|
*
|
||||||
|
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
|
||||||
|
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||||
*
|
*
|
||||||
* @name duckdepth
|
* @name duckdepth
|
||||||
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
||||||
* @example
|
* @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>"))
|
* 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>"))
|
||||||
|
* @example
|
||||||
|
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* $: s("hh*16").orbit(3)
|
||||||
|
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const { duckdepth } = registerControl('duckdepth');
|
export const { duckdepth } = registerControl('duckdepth');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the attack time of the duck effect
|
* The time required for the ducked signal(s) to reach their lowest volume.
|
||||||
|
* Can be used to prevent clicking or for creative rhythmic effects.
|
||||||
|
*
|
||||||
|
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||||
|
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||||
|
*
|
||||||
|
* @name duckonset
|
||||||
|
* @synonyms duckons
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} time The onset time in seconds
|
||||||
|
* @example
|
||||||
|
* // Clicks
|
||||||
|
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||||
|
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
|
||||||
|
* @example
|
||||||
|
* // No clicks
|
||||||
|
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||||
|
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
|
||||||
|
* @example
|
||||||
|
* // Rhythmic
|
||||||
|
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
|
||||||
|
* hhat: s("hh*16").orbit(7)
|
||||||
|
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { duckonset } = registerControl('duckonset', 'duckons');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time required for the ducked signal(s) to return to their normal volume.
|
||||||
|
*
|
||||||
|
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||||
|
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||||
*
|
*
|
||||||
* @name duckattack
|
* @name duckattack
|
||||||
* @param {number | Pattern} time
|
* @synonyms duckatt
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} time The attack time in seconds
|
||||||
* @example
|
* @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))
|
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
|
||||||
|
* @example
|
||||||
|
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* lessduck: s("hh*16").orbit(5)
|
||||||
|
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||||
|
|
||||||
export const { drive } = registerControl('drive');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create byte beats with custom expressions
|
* Create byte beats with custom expressions
|
||||||
*
|
*
|
||||||
@@ -618,7 +916,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
|
|||||||
export const { channels, ch } = registerControl('channels', 'ch');
|
export const { channels, ch } = registerControl('channels', 'ch');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls the pulsewidth of the pulse oscillator
|
* Controls the pulsewidth of the pulse oscillator
|
||||||
*
|
*
|
||||||
* @name pw
|
* @name pw
|
||||||
* @param {number | Pattern} pulsewidth
|
* @param {number | Pattern} pulsewidth
|
||||||
@@ -630,7 +928,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
|
|||||||
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls the lfo rate for the pulsewidth of the pulse oscillator
|
* Controls the lfo rate for the pulsewidth of the pulse oscillator
|
||||||
*
|
*
|
||||||
* @name pwrate
|
* @name pwrate
|
||||||
* @param {number | Pattern} rate
|
* @param {number | Pattern} rate
|
||||||
@@ -642,7 +940,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
|||||||
export const { pwrate } = registerControl('pwrate');
|
export const { pwrate } = registerControl('pwrate');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls the lfo sweep for the pulsewidth of the pulse oscillator
|
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
|
||||||
*
|
*
|
||||||
* @name pwsweep
|
* @name pwsweep
|
||||||
* @param {number | Pattern} sweep
|
* @param {number | Pattern} sweep
|
||||||
@@ -683,7 +981,7 @@ export const { phaserrate, ph, phaser } = registerControl(
|
|||||||
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
|
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The center frequency of the phaser in HZ. Defaults to 1000
|
* The center frequency of the phaser in HZ. Defaults to 1000
|
||||||
*
|
*
|
||||||
* @name phasercenter
|
* @name phasercenter
|
||||||
* @synonyms phc
|
* @synonyms phc
|
||||||
@@ -700,7 +998,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
|||||||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||||
*
|
*
|
||||||
* @name phaserdepth
|
* @name phaserdepth
|
||||||
* @synonyms phd
|
* @synonyms phd, phasdp
|
||||||
* @param {number | Pattern} depth number between 0 and 1
|
* @param {number | Pattern} depth number between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||||
@@ -711,7 +1009,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
|||||||
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
|
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* choose the channel the pattern is sent to in superdirt
|
* Choose the channel the pattern is sent to in superdirt
|
||||||
*
|
*
|
||||||
* @name channel
|
* @name channel
|
||||||
* @param {number | Pattern} channel channel number
|
* @param {number | Pattern} channel channel number
|
||||||
@@ -1063,7 +1361,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
|
|||||||
* @name djf
|
* @name djf
|
||||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
||||||
* @example
|
* @example
|
||||||
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
|
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { djf } = registerControl('djf');
|
export const { djf } = registerControl('djf');
|
||||||
@@ -1097,14 +1395,27 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the level of the signal that is fed back into the delay.
|
||||||
|
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||||
|
*
|
||||||
|
* @name delayfeedback
|
||||||
|
* @param {number | Pattern} feedback between 0 and 1
|
||||||
|
* @synonyms delayfb, dfb
|
||||||
|
* @example
|
||||||
|
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { delayspeed } = registerControl('delayspeed');
|
||||||
/**
|
/**
|
||||||
* Sets the time of the delay effect.
|
* Sets the time of the delay effect.
|
||||||
*
|
*
|
||||||
* @name delaytime
|
* @name delayspeed
|
||||||
* @param {number | Pattern} seconds between 0 and Infinity
|
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
|
||||||
* @synonyms delayt, dt
|
* @synonyms delayt, dt
|
||||||
* @example
|
* @example
|
||||||
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
|
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||||
@@ -1182,6 +1493,7 @@ export const { dry } = registerControl('dry');
|
|||||||
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||||
*
|
*
|
||||||
* @name fadeTime
|
* @name fadeTime
|
||||||
|
* @synonyms fadeOutTime
|
||||||
* @param {number | Pattern} time between 0 and 1
|
* @param {number | Pattern} time between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||||
@@ -1575,19 +1887,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
|
|||||||
export const { shape } = registerControl(['shape', 'shapevol']);
|
export const { shape } = registerControl(['shape', 'shapevol']);
|
||||||
/**
|
/**
|
||||||
* Wave shaping distortion. CAUTION: it can get loud.
|
* Wave shaping distortion. CAUTION: it can get loud.
|
||||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
|
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
|
||||||
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
|
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
|
||||||
*
|
*
|
||||||
* @name distort
|
* @name distort
|
||||||
* @synonyms dist
|
* @synonyms dist
|
||||||
* @param {number | Pattern} distortion
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
* @param {number | string | Pattern} type type of distortion to apply
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
||||||
* @example
|
* @example
|
||||||
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
||||||
|
* @example
|
||||||
|
* s("bd:4*4").bank("tr808").distort("3:0.5:diode")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
|
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postgain for waveshaping distortion.
|
||||||
|
*
|
||||||
|
* @name distortvol
|
||||||
|
* @synonyms distvol
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
* @example
|
||||||
|
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
|
||||||
|
*/
|
||||||
|
export const { distortvol } = registerControl('distortvol', 'distvol');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of waveshaping distortion to apply.
|
||||||
|
*
|
||||||
|
* @name distorttype
|
||||||
|
* @synonyms disttype
|
||||||
|
* @param {number | string | Pattern} type type of distortion to apply
|
||||||
|
* @example
|
||||||
|
* s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>")
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("sine").note("F1*2").release(1)
|
||||||
|
* .penv(24).pdecay(0.05)
|
||||||
|
* .distort(rand.range(1, 8))
|
||||||
|
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
|
||||||
|
*/
|
||||||
|
export const { distorttype } = registerControl('distorttype', 'disttype');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
|
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
|
||||||
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
||||||
@@ -1975,6 +2320,24 @@ export const { miditouch } = registerControl('miditouch');
|
|||||||
// TODO: what is this?
|
// TODO: what is this?
|
||||||
export const { polyTouch } = registerControl('polyTouch');
|
export const { polyTouch } = registerControl('polyTouch');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The host to send open sound control messages to. Requires running the OSC bridge.
|
||||||
|
* @name oschost
|
||||||
|
* @param {string | Pattern} oschost e.g. 'localhost'
|
||||||
|
* @example
|
||||||
|
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
|
||||||
|
*/
|
||||||
|
export const { oschost } = registerControl('oschost');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The port to send open sound control messages to. Requires running the OSC bridge.
|
||||||
|
* @name oscport
|
||||||
|
* @param {number | Pattern} oscport e.g. 57120
|
||||||
|
* @example
|
||||||
|
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
|
||||||
|
*/
|
||||||
|
export const { oscport } = registerControl('oscport');
|
||||||
|
|
||||||
export const getControlName = (alias) => {
|
export const getControlName = (alias) => {
|
||||||
if (controlAlias.has(alias)) {
|
if (controlAlias.has(alias)) {
|
||||||
return controlAlias.get(alias);
|
return controlAlias.get(alias);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// query the pattern for events
|
// query the pattern for events
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
|
|||||||
@@ -35,18 +35,18 @@ const right = function (n, x) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _bjork = function (n, x) {
|
const _bjorklund = function (n, x) {
|
||||||
const [ons, offs] = n;
|
const [ons, offs] = n;
|
||||||
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
|
return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bjork = function (ons, steps) {
|
export const bjorklund = function (ons, steps) {
|
||||||
const inverted = ons < 0;
|
const inverted = ons < 0;
|
||||||
const absOns = Math.abs(ons);
|
const absOns = Math.abs(ons);
|
||||||
const offs = steps - absOns;
|
const offs = steps - absOns;
|
||||||
const ones = Array(absOns).fill([1]);
|
const ones = Array(absOns).fill([1]);
|
||||||
const zeros = Array(offs).fill([0]);
|
const zeros = Array(offs).fill([0]);
|
||||||
const result = _bjork([absOns, offs], [ones, zeros]);
|
const result = _bjorklund([absOns, offs], [ones, zeros]);
|
||||||
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
|
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||||
return inverted ? pattern.map((x) => 1 - x) : pattern;
|
return inverted ? pattern.map((x) => 1 - x) : pattern;
|
||||||
};
|
};
|
||||||
@@ -128,7 +128,7 @@ export const bjork = function (ons, steps) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const _euclidRot = function (pulses, steps, rotation) {
|
const _euclidRot = function (pulses, steps, rotation) {
|
||||||
const b = bjork(pulses, steps);
|
const b = bjorklund(pulses, steps);
|
||||||
if (rotation) {
|
if (rotation) {
|
||||||
return rotate(b, -rotation);
|
return rotate(b, -rotation);
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
|
|||||||
return pat.struct(_euclidRot(pulses, steps, 0));
|
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
export const e = register('e', function (euc, pat) {
|
export const bjork = register('bjork', function (euc, pat) {
|
||||||
if (!Array.isArray(euc)) {
|
if (!Array.isArray(euc)) {
|
||||||
euc = [euc];
|
euc = [euc];
|
||||||
}
|
}
|
||||||
@@ -216,6 +216,6 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
|
|||||||
* .pan(sine.slow(8))
|
* .pan(sine.slow(8))
|
||||||
*/
|
*/
|
||||||
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
||||||
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
|
const morphed = _morph(bjorklund(pulses, steps), new Array(pulses).fill(1), perc);
|
||||||
return pat.struct(morphed).setSteps(steps);
|
return pat.struct(morphed).setSteps(steps);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ export const lcm = (...fractions) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFraction = (x) => x instanceof Fraction;
|
||||||
|
|
||||||
fraction._original = Fraction;
|
fraction._original = Fraction;
|
||||||
|
|
||||||
export default fraction;
|
export default fraction;
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ let debounce = 1000,
|
|||||||
lastTime;
|
lastTime;
|
||||||
|
|
||||||
export function errorLogger(e, origin = 'cyclist') {
|
export function errorLogger(e, origin = 'cyclist') {
|
||||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
if (process.env.NODE_ENV === 'development') {
|
||||||
// console.error(e);
|
console.error(e);
|
||||||
|
}
|
||||||
logger(`[${origin}] error: ${e.message}`);
|
logger(`[${origin}] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ export class NeoCyclist {
|
|||||||
if (this.started === false) {
|
if (this.started === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/core",
|
"name": "@strudel/core",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+153
-12
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import TimeSpan from './timespan.mjs';
|
import TimeSpan from './timespan.mjs';
|
||||||
import Fraction, { lcm } from './fraction.mjs';
|
import Fraction, { isFraction, lcm } from './fraction.mjs';
|
||||||
import Hap from './hap.mjs';
|
import Hap from './hap.mjs';
|
||||||
import State from './state.mjs';
|
import State from './state.mjs';
|
||||||
import { unionWithObj } from './value.mjs';
|
import { unionWithObj } from './value.mjs';
|
||||||
@@ -98,10 +98,7 @@ export class Pattern {
|
|||||||
|
|
||||||
// runs func on query state
|
// runs func on query state
|
||||||
withState(func) {
|
withState(func) {
|
||||||
return this.withHaps((haps, state) => {
|
return new Pattern((state) => this.query(func(state)));
|
||||||
func(state);
|
|
||||||
return haps;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -999,7 +996,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
|
|||||||
// compose matrix functions
|
// compose matrix functions
|
||||||
|
|
||||||
function _nonArrayObject(x) {
|
function _nonArrayObject(x) {
|
||||||
return !Array.isArray(x) && typeof x === 'object';
|
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
|
||||||
}
|
}
|
||||||
function _composeOp(a, b, func) {
|
function _composeOp(a, b, func) {
|
||||||
if (_nonArrayObject(a) || _nonArrayObject(b)) {
|
if (_nonArrayObject(a) || _nonArrayObject(b)) {
|
||||||
@@ -1246,7 +1243,8 @@ export const silence = gap(1);
|
|||||||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||||
export const nothing = gap(0);
|
export const nothing = gap(0);
|
||||||
|
|
||||||
/** A discrete value that repeats once per cycle.
|
/**
|
||||||
|
* A discrete value that repeats once per cycle.
|
||||||
*
|
*
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
@@ -1299,7 +1297,8 @@ export function sequenceP(pats) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The given items are played at the same time at the same length.
|
/**
|
||||||
|
* The given items are played at the same time at the same length.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms polyrhythm, pr
|
* @synonyms polyrhythm, pr
|
||||||
@@ -1382,11 +1381,11 @@ export function stackBy(by, ...pats) {
|
|||||||
.setSteps(steps);
|
.setSteps(steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
/**
|
||||||
*
|
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
||||||
* synonyms: `cat`
|
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @synonyms cat
|
||||||
* @example
|
* @example
|
||||||
* slowcat("e5", "b4", ["d5", "c5"])
|
* slowcat("e5", "b4", ["d5", "c5"])
|
||||||
*
|
*
|
||||||
@@ -2397,6 +2396,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
|
|||||||
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
|
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const applyN = register('applyN', function (n, func, p) {
|
||||||
|
let result = p;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
result = func(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
|
||||||
|
* @name plyWith
|
||||||
|
* @synonyms plywith
|
||||||
|
* @param {number} factor how many times to repeat
|
||||||
|
* @param {function} func function to apply, given the pattern
|
||||||
|
* @example
|
||||||
|
* "<0 [2 4]>"
|
||||||
|
* .plyWith(4, (p) => p.add(2))
|
||||||
|
* .scale("C:minor").note()
|
||||||
|
*/
|
||||||
|
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
|
||||||
|
const result = pat
|
||||||
|
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
|
||||||
|
.squeezeJoin();
|
||||||
|
if (__steps) {
|
||||||
|
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
|
||||||
|
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
|
||||||
|
* @name plyForEach
|
||||||
|
* @synonyms plyforeach
|
||||||
|
* @param {number} factor how many times to repeat
|
||||||
|
* @param {function} func function to apply, given the pattern and the iteration index
|
||||||
|
* @example
|
||||||
|
* "<0 [2 4]>"
|
||||||
|
* .plyForEach(4, (p,n) => p.add(n*2))
|
||||||
|
* .scale("C:minor").note()
|
||||||
|
*/
|
||||||
|
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
|
||||||
|
const result = pat
|
||||||
|
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
|
||||||
|
.squeezeJoin();
|
||||||
|
if (__steps) {
|
||||||
|
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
|
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
|
||||||
* @name iter
|
* @name iter
|
||||||
@@ -2524,8 +2574,8 @@ export const { chunkBack, chunkback } = register(
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* "<0 8> 1 2 3 4 5 6 7"
|
* "<0 8> 1 2 3 4 5 6 7"
|
||||||
* .fastChunk(4, x => x.color('red')).slow(2)
|
|
||||||
* .scale("C2:major").note()
|
* .scale("C2:major").note()
|
||||||
|
* .fastChunk(4, x => x.color('red')).slow(2)
|
||||||
*/
|
*/
|
||||||
export const { fastchunk, fastChunk } = register(
|
export const { fastchunk, fastChunk } = register(
|
||||||
['fastchunk', 'fastChunk'],
|
['fastchunk', 'fastChunk'],
|
||||||
@@ -2948,6 +2998,24 @@ export const extend = stepRegister('extend', function (factor, pat) {
|
|||||||
return pat.fast(factor).expand(factor);
|
return pat.fast(factor).expand(factor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* *Experimental*
|
||||||
|
*
|
||||||
|
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
|
||||||
|
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
|
||||||
|
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
|
||||||
|
*
|
||||||
|
* TODO: find out how this function differs from extend
|
||||||
|
* @example
|
||||||
|
* stepcat(
|
||||||
|
* sound("bd bd - cp").replicate(2),
|
||||||
|
* sound("bd - sd -")
|
||||||
|
* ).pace(8)
|
||||||
|
*/
|
||||||
|
export const replicate = stepRegister('replicate', function (factor, pat) {
|
||||||
|
return pat.repeatCycles(factor).fast(factor).expand(factor);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
@@ -3483,3 +3551,76 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
bypat = reify(bypat);
|
bypat = reify(bypat);
|
||||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-clipping distortion
|
||||||
|
*
|
||||||
|
* @name soft
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Hard-clipping distortion
|
||||||
|
*
|
||||||
|
* @name hard
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Cubic polynomial distortion
|
||||||
|
*
|
||||||
|
* @name cubic
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Diode-emulating distortion
|
||||||
|
*
|
||||||
|
* @name diode
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Asymmetrical diode distortion
|
||||||
|
*
|
||||||
|
* @name asym
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Wavefolding distortion
|
||||||
|
*
|
||||||
|
* @name fold
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Wavefolding distortion composed with sinusoid
|
||||||
|
*
|
||||||
|
* @name sinefold
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Distortion via Chebyshev polynomials
|
||||||
|
*
|
||||||
|
* @name chebyshev
|
||||||
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
||||||
|
for (const name of distAlgoNames) {
|
||||||
|
// Add aliases for distortion algorithms
|
||||||
|
Pattern.prototype[name] = function (args) {
|
||||||
|
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||||
|
return this.distort(argsPat);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
+21
-3
@@ -74,6 +74,14 @@ export function repl({
|
|||||||
return silence;
|
return silence;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// helper to get a patternified pure value out
|
||||||
|
function unpure(pat) {
|
||||||
|
if (pat._Pattern) {
|
||||||
|
return pat.__pure;
|
||||||
|
}
|
||||||
|
return pat;
|
||||||
|
}
|
||||||
|
|
||||||
const setPattern = async (pattern, autostart = true) => {
|
const setPattern = async (pattern, autostart = true) => {
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
await scheduler.setPattern(pattern, autostart);
|
await scheduler.setPattern(pattern, autostart);
|
||||||
@@ -85,7 +93,10 @@ export function repl({
|
|||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
const toggle = () => scheduler.toggle();
|
const toggle = () => scheduler.toggle();
|
||||||
const setCps = (cps) => scheduler.setCps(cps);
|
const setCps = (cps) => {
|
||||||
|
scheduler.setCps(unpure(cps));
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the global tempo to the given cycles per minute
|
* Changes the global tempo to the given cycles per minute
|
||||||
@@ -97,7 +108,10 @@ export function repl({
|
|||||||
* setcpm(140/4) // =140 bpm in 4/4
|
* setcpm(140/4) // =140 bpm in 4/4
|
||||||
* $: s("bd*4,[- sd]*2").bank('tr707')
|
* $: s("bd*4,[- sd]*2").bank('tr707')
|
||||||
*/
|
*/
|
||||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
const setCpm = (cpm) => {
|
||||||
|
scheduler.setCps(unpure(cpm) / 60);
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||||
|
|
||||||
@@ -200,7 +214,10 @@ export function repl({
|
|||||||
}
|
}
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||||
if (Object.keys(pPatterns).length) {
|
if (Object.keys(pPatterns).length) {
|
||||||
let patterns = Object.values(pPatterns);
|
let patterns = [];
|
||||||
|
for (const [key, value] of Object.entries(pPatterns)) {
|
||||||
|
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||||
|
}
|
||||||
if (eachTransform) {
|
if (eachTransform) {
|
||||||
// Explicit lambda so only element (not index and array) are passed
|
// Explicit lambda so only element (not index and array) are passed
|
||||||
patterns = patterns.map((x) => eachTransform(x));
|
patterns = patterns.map((x) => eachTransform(x));
|
||||||
@@ -214,6 +231,7 @@ export function repl({
|
|||||||
pattern = allTransforms[i](pattern);
|
pattern = allTransforms[i](pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPattern(pattern)) {
|
if (!isPattern(pattern)) {
|
||||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export class State {
|
|||||||
return this.setSpan(func(this.span));
|
return this.setSpan(func(this.span));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns new State with different controls
|
// Returns new State with added controls.
|
||||||
setControls(controls) {
|
setControls(controls) {
|
||||||
return new State(this.span, controls);
|
return new State(this.span, { ...this.controls, ...controls });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { bjork } from '../euclid.mjs';
|
import { bjorklund } from '../euclid.mjs';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { fastcat } from '../pattern.mjs';
|
import { fastcat } from '../pattern.mjs';
|
||||||
|
|
||||||
describe('bjork', () => {
|
describe('bjorklund', () => {
|
||||||
it('should apply bjorklund to ons and steps', () => {
|
it('should apply bjorklundlund to ons and steps', () => {
|
||||||
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
||||||
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
expect(bjorklund(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
||||||
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
expect(bjorklund(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
||||||
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
expect(bjorklund(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||||
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
expect(bjorklund(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bsf]*[0-9]*$/.test(name);
|
||||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]*$/.test(name);
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,10 @@ export const tokenizeNote = (note) => {
|
|||||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||||
|
|
||||||
|
export const getAccidentalsOffset = (accidentals) => {
|
||||||
|
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
@@ -30,7 +34,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = chromas[pc.toLowerCase()];
|
const chroma = chromas[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
const offset = getAccidentalsOffset(acc);
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/csound",
|
"name": "@strudel/csound",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -84,9 +84,18 @@ Pattern.prototype.onPaint = function (painter) {
|
|||||||
state.controls.painters = [];
|
state.controls.painters = [];
|
||||||
}
|
}
|
||||||
state.controls.painters.push(painter);
|
state.controls.painters.push(painter);
|
||||||
|
return state;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO - Why isn't this pure deep copy not working?
|
||||||
|
// Pattern.prototype.onPaint = function (painter) {
|
||||||
|
// return this.withState((state) => {
|
||||||
|
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
|
||||||
|
// return new State(state.span, { ...state.controls, painters });
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
Pattern.prototype.getPainters = function () {
|
Pattern.prototype.getPainters = function () {
|
||||||
let painters = [];
|
let painters = [];
|
||||||
this.queryArc(0, 0, { painters });
|
this.queryArc(0, 0, { painters });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/draw",
|
"name": "@strudel/draw",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Helpers for drawing with Strudel",
|
"description": "Helpers for drawing with Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ npm i @strudel/gamepad --save
|
|||||||
import { gamepad } from '@strudel/gamepad';
|
import { gamepad } from '@strudel/gamepad';
|
||||||
|
|
||||||
// Initialize gamepad (optional index parameter, defaults to 0)
|
// Initialize gamepad (optional index parameter, defaults to 0)
|
||||||
const pad = gamepad(0);
|
const pad = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
|
||||||
|
//const pad = gamepad(0, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
//const pad = gamepad(0, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
|
||||||
|
//const pad = gamepad(0, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
|
||||||
|
|
||||||
|
|
||||||
// Use gamepad inputs in patterns
|
// Use gamepad inputs in patterns
|
||||||
const pattern = sequence([
|
const pattern = sequence([
|
||||||
@@ -86,8 +91,35 @@ $: sound("hadoken").gain(pad.checkSequence(HADOKEN))
|
|||||||
## Multiple Gamepads
|
## Multiple Gamepads
|
||||||
|
|
||||||
You can connect multiple gamepads by specifying the gamepad index:
|
You can connect multiple gamepads by specifying the gamepad index:
|
||||||
|
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const pad1 = gamepad(0); // First gamepad
|
const pad1 = gamepad(0); // First gamepad
|
||||||
const pad2 = gamepad(1); // Second gamepad
|
const pad2 = gamepad(1); // Second gamepad
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Vibration
|
||||||
|
|
||||||
|
You can use the `vibrate` control to provide haptic feedback to the gamepad.
|
||||||
|
The `vibrate` control is a patternable control, so you can use it to control the vibration of the gamepad.
|
||||||
|
|
||||||
|
The vibrate control has several parameters:
|
||||||
|
|
||||||
|
- `vibGamepadIndex` - Index of the gamepad to vibrate (0-3)
|
||||||
|
- `vibStrong` - Intensity of the strong/left motor (0-1)
|
||||||
|
- `vibWeak` - Intensity of the weak/right motor (0-1)
|
||||||
|
- `vibDuration` - Duration of vibration in milliseconds
|
||||||
|
- `vibEnable` - Enable/disable vibration (0 or 1)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Vibration with gamepad
|
||||||
|
// vibrate(gamepadId, { strong: 1, weak: 0.5, duration: 100 })
|
||||||
|
// vibrate("gamepadId")
|
||||||
|
// vibrate(gamepadId)
|
||||||
|
|
||||||
|
$: note("c4*4").vibrate(0, { strong: 1, weak: 0.5, duration: 100 }) // gamepad index 0
|
||||||
|
$: note("c4*4").vibrate("0:1:0.5:200") // Array format
|
||||||
|
$: note("c4*4").vibrate("<0 1 0 1>") // select gamepad index
|
||||||
|
|
||||||
|
$: note("c4*4").vibStrong("<0.1 0.2 0.5 1>").vibWeak("<0.1 0.2 0.5 1>").vibDuration("<10 20 50 100>").vibrate(0) // gamepad index 0
|
||||||
|
```
|
||||||
|
|||||||
@@ -107,9 +107,32 @@ $: s("free_hadouken -").slow(2)
|
|||||||
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
|
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
|
||||||
`} />
|
`} />
|
||||||
|
|
||||||
## Multiple Gamepads
|
### Button Sequences
|
||||||
|
|
||||||
|
### Button Mappings
|
||||||
|
|
||||||
|
The gamepad module supports different button mapping configurations to accommodate various gamepad layouts:
|
||||||
|
|
||||||
|
- **XBOX** (Default): Standard Xbox-style mapping where A=0, B=1, X=2, Y=3
|
||||||
|
- **NES**: Nintendo-style mapping where B=0, A=1, Y=2, X=3
|
||||||
|
- **Custom**: Define your own button mapping by passing an object with button assignments
|
||||||
|
|
||||||
|
You can specify the mapping when initializing the gamepad:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`
|
||||||
|
const pad1 = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
const pad2 = gamepad(1, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
const pad3 = gamepad(2, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
|
||||||
|
const pad4 = gamepad(3, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Multiple Gamepads
|
||||||
|
|
||||||
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
|
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
|
||||||
|
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
|
|||||||
+147
-32
@@ -1,35 +1,77 @@
|
|||||||
// @strudel/gamepad/index.mjs
|
// @strudel/gamepad/index.mjs
|
||||||
|
|
||||||
import { signal } from '@strudel/core';
|
import { signal, Pattern, logger, registerControl, register, isPattern } from '@strudel/core';
|
||||||
|
import { vibrationSupported, getGamepadVibrationActuator } from './vibration.mjs';
|
||||||
|
|
||||||
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
|
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
|
||||||
export const buttonMap = {
|
|
||||||
a: 0,
|
export const buttonMapSettings = {
|
||||||
b: 1,
|
XBOX: {
|
||||||
x: 2,
|
// XBOX mapping default
|
||||||
y: 3,
|
a: 0,
|
||||||
lb: 4,
|
b: 1,
|
||||||
rb: 5,
|
x: 2,
|
||||||
lt: 6,
|
y: 3,
|
||||||
rt: 7,
|
lb: 4,
|
||||||
back: 8,
|
rb: 5,
|
||||||
start: 9,
|
lt: 6,
|
||||||
u: 12,
|
rt: 7,
|
||||||
up: 12,
|
back: 8,
|
||||||
d: 13,
|
start: 9,
|
||||||
down: 13,
|
lstick: 10,
|
||||||
l: 14,
|
rstick: 11,
|
||||||
left: 14,
|
ls: 10,
|
||||||
r: 15,
|
rs: 11,
|
||||||
right: 15,
|
u: 12,
|
||||||
|
up: 12,
|
||||||
|
d: 13,
|
||||||
|
down: 13,
|
||||||
|
l: 14,
|
||||||
|
left: 14,
|
||||||
|
r: 15,
|
||||||
|
right: 15,
|
||||||
|
xbox: 16,
|
||||||
|
},
|
||||||
|
NES: {
|
||||||
|
// Nintendo mapping
|
||||||
|
a: 1,
|
||||||
|
b: 0,
|
||||||
|
x: 3,
|
||||||
|
y: 2,
|
||||||
|
lb: 4,
|
||||||
|
rb: 5,
|
||||||
|
zl: 6,
|
||||||
|
zr: 7,
|
||||||
|
lt: 6,
|
||||||
|
rt: 7,
|
||||||
|
back: 8,
|
||||||
|
minus: 8,
|
||||||
|
start: 9,
|
||||||
|
plus: 9,
|
||||||
|
lstick: 10,
|
||||||
|
rstick: 11,
|
||||||
|
ls: 10,
|
||||||
|
rs: 11,
|
||||||
|
u: 12,
|
||||||
|
up: 12,
|
||||||
|
d: 13,
|
||||||
|
down: 13,
|
||||||
|
l: 14,
|
||||||
|
left: 14,
|
||||||
|
r: 15,
|
||||||
|
right: 15,
|
||||||
|
home: 16,
|
||||||
|
select: 17,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class ButtonSequenceDetector {
|
class ButtonSequenceDetector {
|
||||||
constructor(timeWindow = 1000) {
|
constructor(timeWindow = 1000, mapping) {
|
||||||
this.sequence = [];
|
this.sequence = [];
|
||||||
this.timeWindow = timeWindow;
|
this.timeWindow = timeWindow;
|
||||||
this.lastInputTime = 0;
|
this.lastInputTime = 0;
|
||||||
this.buttonStates = Array(16).fill(0); // Track previous state of each button
|
this.buttonStates = Array(Object.keys(mapping).length).fill(0); // Track previous state of each button
|
||||||
|
this.buttonMap = mapping;
|
||||||
// Button mapping for character inputs
|
// Button mapping for character inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +86,8 @@ class ButtonSequenceDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the button name instead of index
|
// Store the button name instead of index
|
||||||
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
|
const buttonName =
|
||||||
|
Object.keys(this.buttonMap).find((key) => this.buttonMap[key] === buttonIndex) || buttonIndex.toString();
|
||||||
|
|
||||||
this.sequence.push({
|
this.sequence.push({
|
||||||
input: buttonName,
|
input: buttonName,
|
||||||
@@ -87,9 +130,9 @@ class ButtonSequenceDetector {
|
|||||||
// Check if either the input matches directly or they refer to the same button in the map
|
// Check if either the input matches directly or they refer to the same button in the map
|
||||||
return (
|
return (
|
||||||
input === target ||
|
input === target ||
|
||||||
buttonMap[input] === buttonMap[target] ||
|
this.buttonMap[input] === this.buttonMap[target] ||
|
||||||
// Also check if the numerical index matches
|
// Also check if the numerical index matches
|
||||||
buttonMap[input] === parseInt(target)
|
this.buttonMap[input] === parseInt(target)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
? 1
|
? 1
|
||||||
@@ -98,12 +141,13 @@ class ButtonSequenceDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GamepadHandler {
|
class GamepadHandler {
|
||||||
constructor(index = 0) {
|
constructor(index = 0, mapping) {
|
||||||
// Add index parameter
|
// Add index parameter
|
||||||
this._gamepads = {};
|
this._gamepads = {};
|
||||||
|
this._mapping = mapping;
|
||||||
this._activeGamepad = index; // Use provided index
|
this._activeGamepad = index; // Use provided index
|
||||||
this._axes = [0, 0, 0, 0];
|
this._axes = [0, 0, 0, 0];
|
||||||
this._buttons = Array(16).fill(0);
|
this._buttons = Array(Object.keys(mapping).length).fill(0);
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,12 +187,66 @@ class GamepadHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add utility function to list all connected gamepads
|
||||||
|
export const listGamepads = () => {
|
||||||
|
const gamepads = navigator.getGamepads();
|
||||||
|
const connectedGamepads = Array.from(gamepads)
|
||||||
|
.filter((gp) => gp !== null)
|
||||||
|
.map((gp) => ({
|
||||||
|
index: gp.index,
|
||||||
|
id: gp.id,
|
||||||
|
mapping: gp.mapping,
|
||||||
|
buttons: gp.buttons.length,
|
||||||
|
axes: gp.axes.length,
|
||||||
|
connected: gp.connected,
|
||||||
|
timestamp: gp.timestamp,
|
||||||
|
}));
|
||||||
|
// Format the gamepads info into a readable string
|
||||||
|
const gamepadsInfo = connectedGamepads.map((gp) => `${gp.index}: ${gp.id}`).join('\n');
|
||||||
|
|
||||||
|
logger(`[gamepad] available gamepads:\n${gamepadsInfo}`);
|
||||||
|
return connectedGamepads;
|
||||||
|
};
|
||||||
|
|
||||||
// Module-level state store for toggle states
|
// Module-level state store for toggle states
|
||||||
const gamepadStates = new Map();
|
const gamepadStates = new Map();
|
||||||
|
|
||||||
export const gamepad = (index = 0) => {
|
export const gamepad = (index = 0, mapping = 'XBOX') => {
|
||||||
const handler = new GamepadHandler(index);
|
// list connected gamepads
|
||||||
const sequenceDetector = new ButtonSequenceDetector(2000);
|
const connectedGamepads = listGamepads();
|
||||||
|
|
||||||
|
// Check if the requested gamepad index exists
|
||||||
|
const requestedGamepad = connectedGamepads.find((gp) => gp.index === index);
|
||||||
|
if (!requestedGamepad) {
|
||||||
|
throw new Error(
|
||||||
|
`[gamepad] gamepad at index ${index} not found. available gamepads: ${connectedGamepads.map((gp) => gp.index).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle button mapping
|
||||||
|
let buttonMap = buttonMapSettings.XBOX;
|
||||||
|
|
||||||
|
if (typeof mapping === 'string') {
|
||||||
|
buttonMap = buttonMapSettings[mapping.toUpperCase()];
|
||||||
|
} else if (typeof mapping === 'object') {
|
||||||
|
buttonMap = { ...buttonMapSettings.XBOX, ...mapping };
|
||||||
|
// Check that all mapping values are valid button indices
|
||||||
|
const maxButtons = requestedGamepad.buttons;
|
||||||
|
for (const [key, value] of Object.entries(mapping)) {
|
||||||
|
if (typeof value !== 'number' || value < 0 || value >= maxButtons) {
|
||||||
|
throw new Error(
|
||||||
|
`[gamepad] invalid button mapping for '${key}': ${value}. Must be a number between 0 and ${maxButtons - 1}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buttonMap) {
|
||||||
|
throw new Error(`[gamepad] button mapping '${mapping}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = new GamepadHandler(index, buttonMap);
|
||||||
|
const sequenceDetector = new ButtonSequenceDetector(2000, buttonMap);
|
||||||
|
|
||||||
// Base signal that polls gamepad state and handles sequence detection
|
// Base signal that polls gamepad state and handles sequence detection
|
||||||
const baseSignal = signal((t) => {
|
const baseSignal = signal((t) => {
|
||||||
@@ -179,7 +277,7 @@ export const gamepad = (index = 0) => {
|
|||||||
axes.y2_2 = axes.y2.toBipolar();
|
axes.y2_2 = axes.y2.toBipolar();
|
||||||
|
|
||||||
// Create button patterns
|
// Create button patterns
|
||||||
const buttons = Array(16)
|
const buttons = Array(Object.keys(buttonMap).length)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, i) => {
|
.map((_, i) => {
|
||||||
// Create unique key for this gamepad+button combination
|
// Create unique key for this gamepad+button combination
|
||||||
@@ -218,8 +316,22 @@ export const gamepad = (index = 0) => {
|
|||||||
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
|
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
|
||||||
};
|
};
|
||||||
const checkSequence = btnSequence;
|
const checkSequence = btnSequence;
|
||||||
|
const sequence = btnSequence;
|
||||||
const btnSeq = btnSequence;
|
const btnSeq = btnSequence;
|
||||||
const btnseq = btnSeq;
|
const btnseq = btnSequence;
|
||||||
|
const seq = btnSequence;
|
||||||
|
|
||||||
|
// Create vibration pattern
|
||||||
|
const vibration = (pattern) => {
|
||||||
|
return signal(() => {
|
||||||
|
// Return vibration enable/disable pattern
|
||||||
|
return pattern;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
logger(
|
||||||
|
`[gamepad] connected to gamepad ${index} (${requestedGamepad.id}) with ${typeof mapping === 'object' ? 'custom' : mapping} mapping${vibrationSupported(index) ? ' (vibration supported)' : ''}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Return an object with all controls
|
// Return an object with all controls
|
||||||
return {
|
return {
|
||||||
@@ -234,9 +346,12 @@ export const gamepad = (index = 0) => {
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
checkSequence,
|
checkSequence,
|
||||||
|
sequence,
|
||||||
btnSequence,
|
btnSequence,
|
||||||
btnSeq,
|
btnSeq,
|
||||||
btnseq,
|
btnseq,
|
||||||
|
seq,
|
||||||
|
vibration,
|
||||||
raw: baseSignal,
|
raw: baseSignal,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/gamepad",
|
"name": "@strudel/gamepad",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Gamepad Inputs for strudel",
|
"description": "Gamepad Inputs for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
// Gamepad Vibration Controls
|
||||||
|
|
||||||
|
import { registerControl, register, logger, isPattern } from '@strudel/core';
|
||||||
|
import { Pattern } from '@strudel/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the strong motor intensity for gamepad vibration.
|
||||||
|
*
|
||||||
|
* @name vibStrong
|
||||||
|
* @param {number | Pattern} intensity between 0 and 1
|
||||||
|
* @synonyms vibS
|
||||||
|
* @example
|
||||||
|
* s("bd hh sd hh").vibStrong("1 0 0.5 0").vibrate(0)
|
||||||
|
*/
|
||||||
|
export const { vibStrong, vibS } = registerControl('vibStrong', 'vibS');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the weak motor intensity for gamepad vibration.
|
||||||
|
*
|
||||||
|
* @name vibWeak
|
||||||
|
* @param {number | Pattern} intensity between 0 and 1
|
||||||
|
* @synonyms vibW
|
||||||
|
* @example
|
||||||
|
* s("bd hh sd hh").vibWeak("0.5 0 0.8 0").vibrate(0)
|
||||||
|
*/
|
||||||
|
export const { vibWeak, vibW } = registerControl('vibWeak', 'vibW');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the duration of gamepad vibration in milliseconds.
|
||||||
|
*
|
||||||
|
* @name vibDuration
|
||||||
|
* @param {number | Pattern} duration in milliseconds
|
||||||
|
* @synonyms vibDur
|
||||||
|
* @example
|
||||||
|
* s("bd hh sd hh").vibDuration("200 0 100 0").vibrate(0)
|
||||||
|
*/
|
||||||
|
export const { vibDuration, vibDur } = registerControl('vibDuration', 'vibDur');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables gamepad vibration for individual events.
|
||||||
|
*
|
||||||
|
* @name vibEnable
|
||||||
|
* @param {number | Pattern} enable 1 to enable, 0 to disable
|
||||||
|
* @synonyms vibE
|
||||||
|
* @example
|
||||||
|
* s("bd hh sd hh").vibEnable("1 0 1 0").vibrate(0)
|
||||||
|
*/
|
||||||
|
export const { vibEnable, vibEn } = registerControl('vibEnable', 'vibEn');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the gamepad index for vibration events.
|
||||||
|
*
|
||||||
|
* @name vibGamepadIndex
|
||||||
|
* @param {number | Pattern} index gamepad index (0-3)
|
||||||
|
* @synonyms vibGpId, vibGamepadIndex, vibGamepadId
|
||||||
|
* @example
|
||||||
|
* s("bd hh sd hh").vibGamepadIndex("0 1 0 1").vibrate()
|
||||||
|
*/
|
||||||
|
export const { vibGamepadIndex, vibGpId, vibGamepadId } = registerControl('vibGamepadIndex', 'vibGpId', 'vibGamepadId');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets both strong and weak vibration intensity at once.
|
||||||
|
*
|
||||||
|
* @name vibIntensity
|
||||||
|
* @param {number | Pattern} intensity between 0 and 1, applied to both motors
|
||||||
|
* @example
|
||||||
|
* s("bd*4").vibIntensity(sine.slow(2).range(0, 1)).vibrate(0)
|
||||||
|
*/
|
||||||
|
export const vibIntensity = register('vibIntensity', (intensity, pat) => {
|
||||||
|
return pat.vibStrong(intensity).vibWeak(intensity);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const vibrationSupported = (index) => {
|
||||||
|
const gamepad = navigator.getGamepads()[index];
|
||||||
|
return gamepad?.vibrationActuator?.type === 'dual-rumble';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGamepadVibrationActuator = (index) => {
|
||||||
|
const gamepad = navigator.getGamepads()[index];
|
||||||
|
if (!gamepad) {
|
||||||
|
throw new Error(`[gamepad] gamepad at index ${index} not found`);
|
||||||
|
}
|
||||||
|
if (!gamepad.vibrationActuator) {
|
||||||
|
throw new Error(`[gamepad] gamepad at index ${index} does not support vibration`);
|
||||||
|
}
|
||||||
|
return gamepad.vibrationActuator;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default vibration latency in milliseconds
|
||||||
|
const DEFAULT_VIBRATION_LATENCY = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses vibration string format "id:strong:weak:duration"
|
||||||
|
* @param {string} vibString - The vibration string to parse
|
||||||
|
* @returns {object} Parsed vibration parameters
|
||||||
|
*/
|
||||||
|
function parseVibrationString(vibString) {
|
||||||
|
if (typeof vibString !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = vibString.split(':');
|
||||||
|
if (parts.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [id, strong, weak, duration] = parts;
|
||||||
|
|
||||||
|
return {
|
||||||
|
vibGamepadIndex: parseInt(id) || 0,
|
||||||
|
vibStrong: parseFloat(strong) || 0.8,
|
||||||
|
vibWeak: parseFloat(weak) || 0.4,
|
||||||
|
vibDuration: parseInt(duration) || 100,
|
||||||
|
vibEnable: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends vibration commands to a gamepad based on pattern events.
|
||||||
|
*
|
||||||
|
* @name vibrate
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number | string | Pattern} gamepadIndex - Index of the gamepad to vibrate (0-3), can be patterned, or vibration string
|
||||||
|
* @param {object} options - Default vibration options
|
||||||
|
* @param {number} options.strong - Default strong motor intensity (0-1)
|
||||||
|
* @param {number} options.weak - Default weak motor intensity (0-1)
|
||||||
|
* @param {number} options.duration - Default duration in milliseconds
|
||||||
|
* @param {number} options.latency - Latency compensation in milliseconds
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* // Basic vibration on beat
|
||||||
|
* s("bd hh sd hh").vibrate(0)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Using hap values for vibration control
|
||||||
|
* s("<bd hh sd hh>").vibStrong("<1 0.1 0.5 0>").vibWeak("<0.5 0 0.8 0>").vibrate(0)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // With default options
|
||||||
|
* s("bd*4").vibrate(0, { strong: 1, weak: 0.5, duration: 100 })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Using gamepad input to control vibration
|
||||||
|
* let gp = gamepad(0);
|
||||||
|
* note("c a f e").gain(gp.a).vibrate(0, { strong: gp.x1, duration: 200 })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Patternable gamepad index - alternate between gamepads
|
||||||
|
* s("bd hh sd hh").vibrate("<0 1 0 1>")
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Complex gamepad switching pattern
|
||||||
|
* s("bd*8").vibrate("0 1 0 [1 0] 1 0 1 0")
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Using control parameter for gamepad index
|
||||||
|
* s("bd hh sd hh").vibGamepad("<0 1 0 1>").vibrate()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Assignable vibration syntax: "id:strong:weak:duration"
|
||||||
|
* s("bd hh sd hh").vibrate("0:1:0.5:200 1:0.8:0.3:150")
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Complex assignable patterns
|
||||||
|
* s("bd*8").vibrate("0:0.2:0.4:100 0:0.2:1:100 1:1:0.5:200 1:0.5:1:150")
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const { vibrate } = registerControl(['vibGamepad', 'vibStrong', 'vibWeak', 'vibDuration']);
|
||||||
|
|
||||||
|
Pattern.prototype.vibrate = function (gamepadIndex, options = {}) {
|
||||||
|
// Default options
|
||||||
|
const defaultOptions = {
|
||||||
|
strong: 0.8,
|
||||||
|
weak: 0.4,
|
||||||
|
duration: 100,
|
||||||
|
latency: DEFAULT_VIBRATION_LATENCY,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If gamepadIndex is a pattern, handle it based on its content
|
||||||
|
if (isPattern(gamepadIndex)) {
|
||||||
|
return this.set(
|
||||||
|
gamepadIndex.fmap((value) => {
|
||||||
|
// Simple gamepad index pattern
|
||||||
|
return { vibGamepadIndex: value };
|
||||||
|
}),
|
||||||
|
).onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||||
|
return vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If gamepadIndex is provided as static value, use it directly
|
||||||
|
// If undefined, will be handled in vibrateHandler (using vibGamepad control or default 0)
|
||||||
|
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||||
|
return vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions, gamepadIndex);
|
||||||
|
}, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to handle the actual vibration logic
|
||||||
|
function vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions, staticGamepadIndex = null) {
|
||||||
|
try {
|
||||||
|
// Determine gamepad index - from hap value or static parameter
|
||||||
|
let gamepadIndex;
|
||||||
|
if (staticGamepadIndex !== null && staticGamepadIndex !== undefined) {
|
||||||
|
gamepadIndex = staticGamepadIndex;
|
||||||
|
} else if (typeof hap.value === 'object' && ('vibGamepadIndex' in hap.value || 'vibGamepad' in hap.value)) {
|
||||||
|
const vibGamepadValue = hap.value.vibGamepadIndex;
|
||||||
|
gamepadIndex = Array.isArray(vibGamepadValue) ? vibGamepadValue[0] : vibGamepadValue;
|
||||||
|
} else {
|
||||||
|
gamepadIndex = 0; // fallback default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate gamepad index
|
||||||
|
gamepadIndex = Math.max(0, Math.min(3, Math.floor(Number(gamepadIndex) || 0)));
|
||||||
|
|
||||||
|
// Check if vibration is supported
|
||||||
|
if (!vibrationSupported(gamepadIndex)) {
|
||||||
|
logger(`[gamepad] vibration not supported on gamepad ${gamepadIndex}`, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vibrationActuator = getGamepadVibrationActuator(gamepadIndex);
|
||||||
|
|
||||||
|
// Extract vibration parameters from hap value or use defaults
|
||||||
|
let vibStrong, vibWeak, vibDuration, vibEnable;
|
||||||
|
|
||||||
|
if (typeof hap.value === 'object') {
|
||||||
|
const vibGamepadValue = hap.value.vibGamepadIndex;
|
||||||
|
|
||||||
|
// Handle array format: [index, strong, weak, duration]
|
||||||
|
if (Array.isArray(vibGamepadValue)) {
|
||||||
|
vibStrong = vibGamepadValue[1] !== undefined ? vibGamepadValue[1] : defaultOptions.strong;
|
||||||
|
vibWeak = vibGamepadValue[2] !== undefined ? vibGamepadValue[2] : defaultOptions.weak;
|
||||||
|
vibDuration = vibGamepadValue[3] !== undefined ? vibGamepadValue[3] : defaultOptions.duration;
|
||||||
|
vibEnable = 1;
|
||||||
|
} else {
|
||||||
|
// Regular object destructuring for non-array values
|
||||||
|
({
|
||||||
|
vibStrong = defaultOptions.strong,
|
||||||
|
vibWeak = defaultOptions.weak,
|
||||||
|
vibDuration = defaultOptions.duration,
|
||||||
|
vibEnable = 1,
|
||||||
|
} = hap.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If hap.value is a primitive, use it as vibEnable (0 = off, 1 = on)
|
||||||
|
vibEnable = hap.value;
|
||||||
|
vibStrong = defaultOptions.strong;
|
||||||
|
vibWeak = defaultOptions.weak;
|
||||||
|
vibDuration = defaultOptions.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip vibration if disabled
|
||||||
|
if (!vibEnable || vibEnable === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
vibStrong = Math.max(0, Math.min(1, Number(vibStrong) || 0));
|
||||||
|
vibWeak = Math.max(0, Math.min(1, Number(vibWeak) || 0));
|
||||||
|
vibDuration = Math.max(0, Number(vibDuration) || defaultOptions.duration);
|
||||||
|
|
||||||
|
// Calculate timing offset
|
||||||
|
const offset = (targetTime - currentTime + defaultOptions.latency / 1000) * 1000;
|
||||||
|
|
||||||
|
// Schedule vibration
|
||||||
|
window.setTimeout(
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
vibrationActuator
|
||||||
|
.playEffect('dual-rumble', {
|
||||||
|
duration: vibDuration,
|
||||||
|
strongMagnitude: vibStrong,
|
||||||
|
weakMagnitude: vibWeak,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger(`[gamepad] vibration failed: ${err.message}`, 'warning');
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[gamepad] vibration error: ${err.message}`, 'warning');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Math.max(0, offset),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[gamepad] vibration setup failed: ${err.message}`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/hydra",
|
"name": "@strudel/hydra",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Hydra integration for strudel",
|
"description": "Hydra integration for strudel",
|
||||||
"main": "hydra.mjs",
|
"main": "hydra.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/midi",
|
"name": "@strudel/midi",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mini",
|
"name": "@strudel/mini",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ export class MondoParser {
|
|||||||
close_curly: /^\}/,
|
close_curly: /^\}/,
|
||||||
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
|
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
|
||||||
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
|
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
|
||||||
op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
|
op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? ..
|
||||||
// dollar: /^\$/,
|
// dollar: /^\$/,
|
||||||
pipe: /^#/,
|
pipe: /^#/,
|
||||||
stack: /^[,$]/,
|
stack: /^[,$]/,
|
||||||
or: /^[|]/,
|
or: /^[|]/,
|
||||||
plain: /^[a-zA-Z0-9-~_^#]+/,
|
plain: /^[a-zA-Z0-9-~_^#]+/,
|
||||||
};
|
};
|
||||||
|
op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
|
||||||
// matches next token
|
// matches next token
|
||||||
next_token(code, offset = 0) {
|
next_token(code, offset = 0) {
|
||||||
for (let type in this.token_types) {
|
for (let type in this.token_types) {
|
||||||
@@ -150,9 +151,9 @@ export class MondoParser {
|
|||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
desugar_ops(children) {
|
desugar_ops(children, types) {
|
||||||
while (true) {
|
while (true) {
|
||||||
let opIndex = children.findIndex((child) => child.type === 'op');
|
let opIndex = children.findIndex((child) => child.type === 'op' && types.includes(child.value));
|
||||||
if (opIndex === -1) break;
|
if (opIndex === -1) break;
|
||||||
const op = { type: 'plain', value: children[opIndex].value };
|
const op = { type: 'plain', value: children[opIndex].value };
|
||||||
if (opIndex === children.length - 1) {
|
if (opIndex === children.length - 1) {
|
||||||
@@ -263,8 +264,10 @@ export class MondoParser {
|
|||||||
// the type we've removed before splitting needs to be added back
|
// the type we've removed before splitting needs to be added back
|
||||||
children = [{ type: 'plain', value: type }, ...children];
|
children = [{ type: 'plain', value: type }, ...children];
|
||||||
}
|
}
|
||||||
children = this.desugar_ops(children);
|
// for each precendence group, call desugar_ops once
|
||||||
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
|
this.op_precedence.forEach((ops) => {
|
||||||
|
children = this.desugar_ops(children, ops);
|
||||||
|
});
|
||||||
children = this.desugar_pipes(children);
|
children = this.desugar_pipes(children);
|
||||||
return children;
|
return children;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ describe('mondo sugar', () => {
|
|||||||
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
|
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: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 x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
|
||||||
|
it('should desugar x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& (: 8 3) bd)'));
|
||||||
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
|
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', () => 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('(x (y z))'));
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import {
|
|||||||
slow,
|
slow,
|
||||||
seq,
|
seq,
|
||||||
stepcat,
|
stepcat,
|
||||||
extend,
|
replicate,
|
||||||
expand,
|
expand,
|
||||||
pace,
|
pace,
|
||||||
chooseIn,
|
chooseIn,
|
||||||
degradeBy,
|
degradeBy,
|
||||||
silence,
|
silence,
|
||||||
|
bjork,
|
||||||
} from '@strudel/core';
|
} from '@strudel/core';
|
||||||
import { registerLanguage } from '@strudel/transpiler';
|
import { registerLanguage } from '@strudel/transpiler';
|
||||||
import { MondoRunner } from 'mondolang';
|
import { MondoRunner } from 'mondolang';
|
||||||
@@ -36,10 +37,11 @@ lib.square = (...args) => stepcat(...args).setSteps(1);
|
|||||||
lib.angle = (...args) => stepcat(...args).pace(1);
|
lib.angle = (...args) => stepcat(...args).pace(1);
|
||||||
lib['*'] = fast;
|
lib['*'] = fast;
|
||||||
lib['/'] = slow;
|
lib['/'] = slow;
|
||||||
lib['!'] = extend;
|
lib['!'] = replicate;
|
||||||
lib['@'] = expand;
|
lib['@'] = expand;
|
||||||
lib['%'] = pace;
|
lib['%'] = pace;
|
||||||
lib['?'] = degradeBy; // todo: default 0.5 not working..
|
lib['?'] = degradeBy; // todo: default 0.5 not working..
|
||||||
|
lib['&'] = bjork;
|
||||||
lib[':'] = tail;
|
lib[':'] = tail;
|
||||||
lib['..'] = range;
|
lib['..'] = range;
|
||||||
lib['def'] = () => silence;
|
lib['def'] = () => silence;
|
||||||
@@ -85,7 +87,7 @@ function evaluator(node, scope) {
|
|||||||
let pat;
|
let pat;
|
||||||
if (type === 'plain' && typeof variable !== 'undefined') {
|
if (type === 'plain' && typeof variable !== 'undefined') {
|
||||||
// some function names are not patternable, so we skip reification here
|
// some function names are not patternable, so we skip reification here
|
||||||
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
|
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
|
||||||
return variable;
|
return variable;
|
||||||
}
|
}
|
||||||
pat = reify(variable);
|
pat = reify(variable);
|
||||||
@@ -108,7 +110,7 @@ export function mondo(code, offset = 0) {
|
|||||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||||
}
|
}
|
||||||
|
|
||||||
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||||
|
|
||||||
export const mondi = (str, offset) => {
|
export const mondi = (str, offset) => {
|
||||||
const code = `[${str}]`;
|
const code = `[${str}]`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mondo",
|
"name": "@strudel/mondo",
|
||||||
"version": "1.1.2",
|
"version": "1.1.5",
|
||||||
"description": "mondo notation for strudel",
|
"description": "mondo notation for strudel",
|
||||||
"main": "mondough.mjs",
|
"main": "mondough.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
//import { dependencies } from './package.json';
|
import { dependencies } from './package.json';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
// external: [...Object.keys(dependencies)],
|
external: [...Object.keys(dependencies)],
|
||||||
},
|
},
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/motion",
|
"name": "@strudel/motion",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "DeviceMotion API for strudel",
|
"description": "DeviceMotion API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function (
|
|||||||
cx.connect(props);
|
cx.connect(props);
|
||||||
}
|
}
|
||||||
return this.withHap((hap) => {
|
return this.withHap((hap) => {
|
||||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
||||||
let msg_topic = topic;
|
let msg_topic = topic;
|
||||||
if (!cx || !cx.isConnected()) {
|
if (!cx || !cx.isConnected()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mqtt",
|
"name": "@strudel/mqtt",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "MQTT API for strudel",
|
"description": "MQTT API for strudel",
|
||||||
"main": "mqtt.mjs",
|
"main": "mqtt.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+27
-17
@@ -4,21 +4,13 @@ OSC output for strudel patterns! Currently only tested with super collider / sup
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
OSC will only work if you run the REPL locally + the OSC server besides it:
|
Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via:
|
||||||
|
|
||||||
From the project root:
|
```sh
|
||||||
|
npx @strudel/osc
|
||||||
```js
|
|
||||||
npm run repl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
and in a seperate shell:
|
You should see something like:
|
||||||
|
|
||||||
```js
|
|
||||||
npm run osc
|
|
||||||
```
|
|
||||||
|
|
||||||
This should give you
|
|
||||||
|
|
||||||
```log
|
```log
|
||||||
osc client running on port 57120
|
osc client running on port 57120
|
||||||
@@ -26,14 +18,32 @@ osc server running on port 57121
|
|||||||
websocket server running on port 8080
|
websocket server running on port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Now open Supercollider (with the super dirt startup file)
|
### --port
|
||||||
|
|
||||||
Now open the REPL and type:
|
By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option:
|
||||||
|
|
||||||
```js
|
```sh
|
||||||
s("<bd sd> hh").osc()
|
npx @strudel/osc --port 7771 # classic dirt
|
||||||
```
|
```
|
||||||
|
|
||||||
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
### --debug
|
||||||
|
|
||||||
|
To log all incoming osc messages, add the `--debug` flag:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx @strudel/osc --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Strudel
|
||||||
|
|
||||||
|
To test it in strudel, you have can use `all(osc)` to send all events through osc:
|
||||||
|
|
||||||
|
```js
|
||||||
|
$: s("bd*4")
|
||||||
|
|
||||||
|
all(osc)
|
||||||
|
```
|
||||||
|
|
||||||
|
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
|
||||||
|
|
||||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
||||||
|
|||||||
+19
-20
@@ -4,28 +4,25 @@ 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/>.
|
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 OSC from 'osc-js';
|
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
||||||
|
|
||||||
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
function connect() {
|
function connect() {
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
// make sure this runs only once
|
// make sure this runs only once
|
||||||
connection = new Promise((resolve, reject) => {
|
connection = new Promise((resolve, reject) => {
|
||||||
const osc = new OSC();
|
const ws = new WebSocket('ws://localhost:8080');
|
||||||
osc.open();
|
ws.addEventListener('open', (event) => {
|
||||||
osc.on('open', () => {
|
logger(`[osc] websocket connected`);
|
||||||
const url = osc.options?.plugin?.socket?.url;
|
resolve(ws);
|
||||||
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
|
|
||||||
resolve(osc);
|
|
||||||
});
|
});
|
||||||
osc.on('close', () => {
|
ws.addEventListener('close', (event) => {
|
||||||
|
logger(`[osc] websocket closed`);
|
||||||
connection = undefined; // allows new connection afterwards
|
connection = undefined; // allows new connection afterwards
|
||||||
console.log('[osc] disconnected');
|
console.log('[osc] disconnected');
|
||||||
reject('OSC connection closed');
|
reject('OSC connection closed');
|
||||||
});
|
});
|
||||||
osc.on('error', (err) => reject(err));
|
ws.addEventListener('error', (err) => reject(err));
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
connection = undefined;
|
connection = undefined;
|
||||||
throw new Error('Could not connect to OSC server. Is it running?');
|
throw new Error('Could not connect to OSC server. Is it running?');
|
||||||
@@ -61,15 +58,19 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
const collator = new ClockCollator({});
|
const collator = new ClockCollator({});
|
||||||
|
|
||||||
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||||
const osc = await connect();
|
const ws = await connect();
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const keyvals = Object.entries(controls).flat();
|
const keyvals = Object.entries(controls).flat();
|
||||||
|
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
|
||||||
|
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
|
||||||
|
|
||||||
const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
|
if ('oschost' in hap.value) {
|
||||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
msg['host'] = hap.value['oschost'];
|
||||||
const bundle = new OSC.Bundle([message], ts);
|
}
|
||||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
if ('oscport' in hap.value) {
|
||||||
osc.send(bundle);
|
msg['port'] = hap.value['oscport'];
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,6 +82,4 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
|
||||||
return this.onTrigger(oscTrigger);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/osc",
|
"name": "@strudel/osc",
|
||||||
"version": "1.2.4",
|
"version": "1.3.0",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
|
"bin": "./server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.mjs"
|
"main": "dist/index.mjs"
|
||||||
@@ -37,7 +38,8 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"osc-js": "^2.4.1"
|
"osc": "^2.4.5",
|
||||||
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
|
|||||||
Regular → Executable
+54
-24
@@ -1,34 +1,64 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/*
|
/*
|
||||||
server.js - <short description TODO>
|
server.js - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This 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 OSC from 'osc-js';
|
// import OSC from 'osc-js';
|
||||||
|
|
||||||
const config = {
|
import { WebSocketServer } from 'ws';
|
||||||
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
import osc from 'osc';
|
||||||
udpServer: {
|
|
||||||
host: 'localhost', // @param {string} Hostname of udp server to bind to
|
|
||||||
port: 57121, // @param {number} Port of udp client for messaging
|
|
||||||
// enabling the following line will receive tidal messages:
|
|
||||||
// port: 57120, // @param {number} Port of udp client for messaging
|
|
||||||
exclusive: false, // @param {boolean} Exclusive flag
|
|
||||||
},
|
|
||||||
udpClient: {
|
|
||||||
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
|
||||||
port: 57120, // @param {number} Port of udp client for messaging
|
|
||||||
},
|
|
||||||
wsServer: {
|
|
||||||
host: 'localhost', // @param {string} Hostname of WebSocket server
|
|
||||||
port: 8080, // @param {number} Port of WebSocket server
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
const WS_PORT = 8080; // WebSocket server port
|
||||||
|
const OSC_REMOTE_IP = '127.0.0.1';
|
||||||
|
const OSC_REMOTE_PORT = 57120;
|
||||||
|
|
||||||
osc.open(); // start a WebSocket server on port 8080
|
const udpPort = new osc.UDPPort({
|
||||||
|
localAddress: '0.0.0.0',
|
||||||
|
localPort: 0,
|
||||||
|
remoteAddress: OSC_REMOTE_IP,
|
||||||
|
remotePort: OSC_REMOTE_PORT,
|
||||||
|
});
|
||||||
|
|
||||||
console.log('osc client running on port', config.udpClient.port);
|
udpPort.open();
|
||||||
console.log('osc server running on port', config.udpServer.port);
|
console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
|
||||||
console.log('websocket server running on port', config.wsServer.port);
|
|
||||||
|
udpPort.on('error', (e) => {
|
||||||
|
console.log('Error: ', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ port: WS_PORT });
|
||||||
|
console.log(`[Listening WS] ws://localhost:${WS_PORT}`);
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
console.log('New WebSocket connection');
|
||||||
|
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
let osc_host = '127.0.0.1';
|
||||||
|
let osc_port = 57120;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
if ('host' in data) {
|
||||||
|
osc_host = data['host'];
|
||||||
|
}
|
||||||
|
if ('port' in data) {
|
||||||
|
osc_port = data['port'];
|
||||||
|
}
|
||||||
|
let msg = { address: data['address'], args: data['args'] };
|
||||||
|
if ('timestamp' in data) {
|
||||||
|
msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
|
||||||
|
}
|
||||||
|
|
||||||
|
udpPort.send(msg, osc_host, osc_port);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing message:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/repl",
|
"name": "@strudel/repl",
|
||||||
"version": "1.2.5",
|
"version": "1.2.7",
|
||||||
"description": "Strudel REPL as a Web Component",
|
"description": "Strudel REPL as a Web Component",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ export async function prebake() {
|
|||||||
// import('@strudel/osc'),
|
// import('@strudel/osc'),
|
||||||
);
|
);
|
||||||
// load samples
|
// load samples
|
||||||
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
|
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main';
|
||||||
|
|
||||||
// TODO: move this onto the strudel repo
|
// TODO: move this onto the strudel repo
|
||||||
const ts = 'https://raw.githubusercontent.com/todepond/samples/main/';
|
const ts = 'https://raw.githubusercontent.com/todepond/samples/main';
|
||||||
|
|
||||||
|
const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main';
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
modulesLoading,
|
modulesLoading,
|
||||||
registerSynthSounds(),
|
registerSynthSounds(),
|
||||||
@@ -36,9 +39,9 @@ export async function prebake() {
|
|||||||
samples(`${ds}/tidal-drum-machines.json`),
|
samples(`${ds}/tidal-drum-machines.json`),
|
||||||
samples(`${ds}/piano.json`),
|
samples(`${ds}/piano.json`),
|
||||||
samples(`${ds}/Dirt-Samples.json`),
|
samples(`${ds}/Dirt-Samples.json`),
|
||||||
samples(`${ds}/uzu-drumkit.json`),
|
|
||||||
samples(`${ds}/vcsl.json`),
|
samples(`${ds}/vcsl.json`),
|
||||||
samples(`${ds}/mridangam.json`),
|
samples(`${ds}/mridangam.json`),
|
||||||
|
samples(`${tc}/strudel.json`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/serial",
|
"name": "@strudel/serial",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Webserial API for strudel",
|
"description": "Webserial API for strudel",
|
||||||
"main": "serial.mjs",
|
"main": "serial.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/soundfonts",
|
"name": "@strudel/soundfonts",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples')
|
|||||||
|
|
||||||
The format is `github:<user>/<repo>/<branch>`.
|
The format is `github:<user>/<repo>/<branch>`.
|
||||||
|
|
||||||
|
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
|
||||||
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
||||||
|
|
||||||
The format is also expected to be the same as explained above.
|
The format is also expected to be the same as explained above.
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
let audioContext;
|
||||||
|
|
||||||
|
export const setDefaultAudioContext = () => {
|
||||||
|
audioContext = new AudioContext();
|
||||||
|
return audioContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAudioContext = () => {
|
||||||
|
if (!audioContext) {
|
||||||
|
return setDefaultAudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAudioContextCurrentTime() {
|
||||||
|
return getAudioContext().currentTime;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
let worklet;
|
let worklet;
|
||||||
export async function dspWorklet(ac, code) {
|
export async function dspWorklet(ac, code) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { clamp, nanFallback } from './util.mjs';
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
||||||
@@ -10,6 +11,13 @@ export function gainNode(value) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function effectSend(input, effect, wet) {
|
||||||
|
const send = gainNode(wet);
|
||||||
|
input.connect(send);
|
||||||
|
send.connect(effect);
|
||||||
|
return send;
|
||||||
|
}
|
||||||
|
|
||||||
const getSlope = (y1, y2, x1, x2) => {
|
const getSlope = (y1, y2, x1, x2) => {
|
||||||
const denom = x2 - x1;
|
const denom = x2 - x1;
|
||||||
if (denom === 0) {
|
if (denom === 0) {
|
||||||
@@ -21,7 +29,9 @@ const getSlope = (y1, y2, x1, x2) => {
|
|||||||
export function getWorklet(ac, processor, params, config) {
|
export function getWorklet(ac, processor, params, config) {
|
||||||
const node = new AudioWorkletNode(ac, processor, config);
|
const node = new AudioWorkletNode(ac, processor, config);
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
node.parameters.get(key).value = value;
|
if (value !== undefined) {
|
||||||
|
node.parameters.get(key).value = value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -88,6 +98,35 @@ export const getParamADSR = (
|
|||||||
param[ramp](min, end + release);
|
param[ramp](min, end + release);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 function getLfo(audioContext, begin, end, properties = {}) {
|
||||||
|
const { shape = 0, ...props } = properties;
|
||||||
|
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||||
|
const lfoprops = {
|
||||||
|
frequency: 1,
|
||||||
|
depth,
|
||||||
|
skew: 0.5,
|
||||||
|
phaseoffset: 0,
|
||||||
|
time: begin,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
shape: getModulationShapeInput(shape),
|
||||||
|
dcoffset,
|
||||||
|
min: dcoffset * depth,
|
||||||
|
max: dcoffset * depth + depth,
|
||||||
|
curve: 1,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||||
const options = {
|
const options = {
|
||||||
threshold: threshold ?? -3,
|
threshold: threshold ?? -3,
|
||||||
@@ -115,6 +154,41 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
|
|||||||
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// helper utility for applying standard modulators to a parameter
|
||||||
|
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
|
||||||
|
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
|
||||||
|
|
||||||
|
if (amount == null) {
|
||||||
|
const hasADSRParams = values.some((p) => p != null);
|
||||||
|
amount = hasADSRParams ? defaultAmount : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = offset ?? 0;
|
||||||
|
const max = amount + min;
|
||||||
|
const diff = Math.abs(max - min);
|
||||||
|
if (diff) {
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
|
||||||
|
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
|
||||||
|
}
|
||||||
|
let lfo;
|
||||||
|
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
|
||||||
|
|
||||||
|
if (depth == null) {
|
||||||
|
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
|
||||||
|
depth = hasLFOParams ? defaultDepth : 0;
|
||||||
|
}
|
||||||
|
if (depth) {
|
||||||
|
lfo = getLfo(audioContext, start, end, {
|
||||||
|
depth,
|
||||||
|
dcoffset,
|
||||||
|
...getLfoInputs,
|
||||||
|
});
|
||||||
|
lfo.connect(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { lfo, disconnect: () => lfo?.disconnect() };
|
||||||
|
}
|
||||||
|
|
||||||
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
|
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
|
||||||
const curve = 'exponential';
|
const curve = 'exponential';
|
||||||
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
||||||
@@ -174,7 +248,7 @@ let curves = ['linear', 'exponential'];
|
|||||||
export function getPitchEnvelope(param, value, t, holdEnd) {
|
export function getPitchEnvelope(param, value, t, holdEnd) {
|
||||||
// envelope is active when any of these values is set
|
// envelope is active when any of these values is set
|
||||||
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
||||||
if (!hasEnvelope) {
|
if (hasEnvelope === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const penv = nanFallback(value.penv, 1, true);
|
const penv = nanFallback(value.penv, 1, true);
|
||||||
@@ -307,3 +381,121 @@ export function applyFM(param, value, begin) {
|
|||||||
}
|
}
|
||||||
return { stop };
|
return { stop };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Saturation curves
|
||||||
|
|
||||||
|
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
|
||||||
|
const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
|
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
|
||||||
|
const _soft = (x, k) => Math.tanh(x * (1 + k));
|
||||||
|
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
|
||||||
|
|
||||||
|
const _fold = (x, k) => {
|
||||||
|
// Closed form folding for audio rate
|
||||||
|
let y = (1 + 0.5 * k) * x;
|
||||||
|
const window = _mod(y + 1, 4);
|
||||||
|
return 1 - Math.abs(window - 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k));
|
||||||
|
|
||||||
|
const _cubic = (x, k) => {
|
||||||
|
const t = __squash(Math.log1p(k));
|
||||||
|
const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1)
|
||||||
|
return _soft(cubic, k);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _diode = (x, k, asym = false) => {
|
||||||
|
const g = 1 + 2 * k; // gain
|
||||||
|
const t = __squash(Math.log1p(k));
|
||||||
|
const bias = 0.07 * t;
|
||||||
|
const pos = _soft(x + bias, 2 * k);
|
||||||
|
const neg = _soft(asym ? bias : -x + bias, 2 * k);
|
||||||
|
const y = pos - neg;
|
||||||
|
// We divide by the derivative at 0 so that the distortion is roughly
|
||||||
|
// the identity map near 0 => small values are preserved and undistorted
|
||||||
|
const sech = 1 / Math.cosh(g * bias);
|
||||||
|
const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2
|
||||||
|
const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x
|
||||||
|
return _soft(y / denom, k);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _asym = (x, k) => _diode(x, k, true);
|
||||||
|
|
||||||
|
const _chebyshev = (x, k) => {
|
||||||
|
const kl = 10 * Math.log1p(k);
|
||||||
|
let tnm1 = 1;
|
||||||
|
let tnm2 = x;
|
||||||
|
let tn;
|
||||||
|
let y = 0;
|
||||||
|
for (let i = 1; i < 64; i++) {
|
||||||
|
if (i < 2) {
|
||||||
|
// Already set inital conditions
|
||||||
|
y += i == 0 ? tnm1 : tnm2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition
|
||||||
|
tnm2 = tnm1;
|
||||||
|
tnm1 = tn;
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
y += Math.min((1.3 * kl) / i, 2) * tn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Soft clip
|
||||||
|
return _soft(y, kl / 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const distortionAlgorithms = {
|
||||||
|
scurve: _scurve,
|
||||||
|
soft: _soft,
|
||||||
|
hard: _hard,
|
||||||
|
cubic: _cubic,
|
||||||
|
diode: _diode,
|
||||||
|
asym: _asym,
|
||||||
|
fold: _fold,
|
||||||
|
sinefold: _sineFold,
|
||||||
|
chebyshev: _chebyshev,
|
||||||
|
};
|
||||||
|
const _algoNames = Object.freeze(Object.keys(distortionAlgorithms));
|
||||||
|
|
||||||
|
export const getDistortionAlgorithm = (algo) => {
|
||||||
|
let index = algo;
|
||||||
|
if (typeof algo === 'string') {
|
||||||
|
index = _algoNames.indexOf(algo);
|
||||||
|
if (index === -1) {
|
||||||
|
logger(`[superdough] Could not find waveshaping algorithm ${algo}.
|
||||||
|
Available options are ${_algoNames.join(', ')}.
|
||||||
|
Defaulting to ${_algoNames[0]}.`);
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number
|
||||||
|
return distortionAlgorithms[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDistortion = (distort, postgain, algorithm) => {
|
||||||
|
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||||
|
let { note, freq } = value;
|
||||||
|
note = note || defaultNote;
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
note = noteToMidi(note); // e.g. c3 => 48
|
||||||
|
}
|
||||||
|
// get frequency
|
||||||
|
if (!freq && typeof note === 'number') {
|
||||||
|
freq = midiToFreq(note); // + 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(freq);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const destroyAudioWorkletNode = (node) => {
|
||||||
|
if (node == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.disconnect();
|
||||||
|
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export * from './synth.mjs';
|
|||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
|
export * from './audioContext.mjs';
|
||||||
|
export * from './wavetable.mjs';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
let log = (msg) => console.log(msg);
|
let log = (msg) => console.log(msg);
|
||||||
|
|
||||||
export function errorLogger(e, origin = 'cyclist') {
|
export function errorLogger(e, origin = 'superdough') {
|
||||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
if (process.env.NODE_ENV === 'development') {
|
||||||
// console.error(e);
|
console.error(e);
|
||||||
|
}
|
||||||
logger(`[${origin}] error: ${e.message}`);
|
logger(`[${origin}] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { drywet } from './helpers.mjs';
|
import { drywet } from './helpers.mjs';
|
||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
let noiseCache = {};
|
let noiseCache = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"name": "superdough",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
|
import { getCommonSampleInfo } from './util.mjs';
|
||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
@@ -22,39 +23,16 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
|
||||||
export function getSampleInfo(hapValue, bank) {
|
export function getSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0, speed = 1.0 } = hapValue;
|
const { speed = 1.0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||||
let transpose = midi - 36; // C3 is middle C;
|
|
||||||
let sampleUrl;
|
|
||||||
let index = 0;
|
|
||||||
if (Array.isArray(bank)) {
|
|
||||||
index = getSoundIndex(n, bank.length);
|
|
||||||
sampleUrl = bank[index];
|
|
||||||
} else {
|
|
||||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
|
||||||
// object format will expect keys as notes
|
|
||||||
const closest = Object.keys(bank)
|
|
||||||
.filter((k) => !k.startsWith('_'))
|
|
||||||
.reduce(
|
|
||||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
|
||||||
index = getSoundIndex(n, bank[closest].length);
|
|
||||||
sampleUrl = bank[closest][index];
|
|
||||||
}
|
|
||||||
const label = `${s}:${index}`;
|
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
return { transpose, sampleUrl, index, midi, label, playbackRate };
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
// takes hapValue and returns buffer + playbackRate.
|
// takes hapValue and returns buffer + playbackRate.
|
||||||
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||||
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||||
if (resolveUrl) {
|
if (resolveUrl) {
|
||||||
sampleUrl = await resolveUrl(sampleUrl);
|
sampleUrl = await resolveUrl(sampleUrl);
|
||||||
}
|
}
|
||||||
@@ -79,14 +57,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
|||||||
bufferSource.buffer = buffer;
|
bufferSource.buffer = buffer;
|
||||||
bufferSource.playbackRate.value = playbackRate;
|
bufferSource.playbackRate.value = playbackRate;
|
||||||
|
|
||||||
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||||
|
|
||||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||||
// the midway point through a 10-second audio buffer is still 5."
|
// the midway point through a 10-second audio buffer is still 5."
|
||||||
const offset = begin * bufferSource.buffer.duration;
|
const offset = begin * bufferSource.buffer.duration;
|
||||||
|
|
||||||
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
|
const loop = hapValue.loop;
|
||||||
if (loop) {
|
if (loop) {
|
||||||
bufferSource.loop = true;
|
bufferSource.loop = true;
|
||||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||||
@@ -143,13 +121,18 @@ function githubPath(base, subpath = '') {
|
|||||||
if (!base.startsWith('github:')) {
|
if (!base.startsWith('github:')) {
|
||||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
}
|
}
|
||||||
let [_, path] = base.split('github:');
|
let path = base.slice('github:'.length);
|
||||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
if (path.split('/').length === 2) {
|
|
||||||
// assume main as default branch if none set
|
let components = path.split('/');
|
||||||
path += '/main';
|
let user = components[0];
|
||||||
}
|
let repo = components.length >= 2 ? components[1] : 'samples';
|
||||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
let branch = components.length >= 3 ? components[2] : 'main';
|
||||||
|
let other = components.slice(3);
|
||||||
|
other.push(subpath ? subpath : '');
|
||||||
|
other = other.join('/');
|
||||||
|
|
||||||
|
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||||
@@ -196,6 +179,52 @@ function getSamplesPrefixHandler(url) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchSampleMap(url) {
|
||||||
|
// check if custom prefix handler
|
||||||
|
const handler = getSamplesPrefixHandler(url);
|
||||||
|
if (handler) {
|
||||||
|
return handler(url);
|
||||||
|
}
|
||||||
|
url = resolveSpecialPaths(url);
|
||||||
|
if (url.startsWith('github:')) {
|
||||||
|
url = githubPath(url, 'strudel.json');
|
||||||
|
}
|
||||||
|
if (url.startsWith('local:')) {
|
||||||
|
url = `http://localhost:5432`;
|
||||||
|
}
|
||||||
|
if (url.startsWith('shabda:')) {
|
||||||
|
let [_, path] = url.split('shabda:');
|
||||||
|
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||||
|
}
|
||||||
|
if (url.startsWith('shabda/speech')) {
|
||||||
|
let [_, path] = url.split('shabda/speech');
|
||||||
|
path = path.startsWith('/') ? path.substring(1) : path;
|
||||||
|
let [params, words] = path.split(':');
|
||||||
|
let gender = 'f';
|
||||||
|
let language = 'en-GB';
|
||||||
|
if (params) {
|
||||||
|
[language, gender] = params.split('/');
|
||||||
|
}
|
||||||
|
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||||
|
}
|
||||||
|
if (typeof fetch !== 'function') {
|
||||||
|
// not a browser
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base = url.split('/').slice(0, -1).join('/');
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
// skip fetch when in node / testing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = await fetch(url)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`error loading "${url}"`);
|
||||||
|
});
|
||||||
|
return [json, json._base || base];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a collection of samples to use with `s`
|
* Loads a collection of samples to use with `s`
|
||||||
* @example
|
* @example
|
||||||
@@ -217,61 +246,16 @@ function getSamplesPrefixHandler(url) {
|
|||||||
|
|
||||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||||
if (typeof sampleMap === 'string') {
|
if (typeof sampleMap === 'string') {
|
||||||
// check if custom prefix handler
|
const [json, base] = await fetchSampleMap(sampleMap);
|
||||||
const handler = getSamplesPrefixHandler(sampleMap);
|
return samples(json, baseUrl || base, options);
|
||||||
if (handler) {
|
|
||||||
return handler(sampleMap);
|
|
||||||
}
|
|
||||||
sampleMap = resolveSpecialPaths(sampleMap);
|
|
||||||
if (sampleMap.startsWith('github:')) {
|
|
||||||
sampleMap = githubPath(sampleMap, 'strudel.json');
|
|
||||||
}
|
|
||||||
if (sampleMap.startsWith('local:')) {
|
|
||||||
sampleMap = `http://localhost:5432`;
|
|
||||||
}
|
|
||||||
if (sampleMap.startsWith('shabda:')) {
|
|
||||||
let [_, path] = sampleMap.split('shabda:');
|
|
||||||
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
|
||||||
}
|
|
||||||
if (sampleMap.startsWith('shabda/speech')) {
|
|
||||||
let [_, path] = sampleMap.split('shabda/speech');
|
|
||||||
path = path.startsWith('/') ? path.substring(1) : path;
|
|
||||||
let [params, words] = path.split(':');
|
|
||||||
let gender = 'f';
|
|
||||||
let language = 'en-GB';
|
|
||||||
if (params) {
|
|
||||||
[language, gender] = params.split('/');
|
|
||||||
}
|
|
||||||
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
|
||||||
}
|
|
||||||
if (typeof fetch !== 'function') {
|
|
||||||
// not a browser
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const base = sampleMap.split('/').slice(0, -1).join('/');
|
|
||||||
if (typeof fetch === 'undefined') {
|
|
||||||
// skip fetch when in node / testing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return fetch(sampleMap)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((json) => samples(json, baseUrl || json._base || base, options))
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
throw new Error(`error loading "${sampleMap}"`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const { prebake, tag } = options;
|
const { prebake, tag } = options;
|
||||||
|
|
||||||
processSampleMap(
|
processSampleMap(
|
||||||
sampleMap,
|
sampleMap,
|
||||||
(key, bank) =>
|
(key, bank) => {
|
||||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
registerSampleSource(key, bank, { baseUrl, prebake, tag });
|
||||||
type: 'sample',
|
},
|
||||||
samples: bank,
|
|
||||||
baseUrl,
|
|
||||||
prebake,
|
|
||||||
tag,
|
|
||||||
}),
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -361,3 +345,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
|
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerSample(key, bank, params) {
|
||||||
|
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||||
|
type: 'sample',
|
||||||
|
samples: bank,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerSampleSource(key, bank, params) {
|
||||||
|
const isWavetable = key.startsWith('wt_');
|
||||||
|
if (isWavetable) {
|
||||||
|
registerWaveTable(key, bank, params);
|
||||||
|
} else {
|
||||||
|
registerSample(key, bank, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
|
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
|
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger, errorLogger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
||||||
@@ -28,13 +30,6 @@ export function setMultiChannelOrbits(bool) {
|
|||||||
multiChannelOrbits = bool == true;
|
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 const soundMap = map();
|
||||||
|
|
||||||
export function registerSound(key, onTrigger, data = {}) {
|
export function registerSound(key, onTrigger, data = {}) {
|
||||||
@@ -113,6 +108,19 @@ export async function aliasBank(...args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an alias for a sound.
|
||||||
|
* @param {string} original - The original sound name
|
||||||
|
* @param {string} alias - The alias to use for the sound
|
||||||
|
*/
|
||||||
|
export function soundAlias(original, alias) {
|
||||||
|
if (getSound(original) == null) {
|
||||||
|
logger('soundAlias: original sound not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
soundMap.setKey(alias, getSound(original));
|
||||||
|
}
|
||||||
|
|
||||||
export function getSound(s) {
|
export function getSound(s) {
|
||||||
if (typeof s !== 'string') {
|
if (typeof s !== 'string') {
|
||||||
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
||||||
@@ -147,6 +155,7 @@ let defaultDefaultValues = {
|
|||||||
phaserdepth: 0.75,
|
phaserdepth: 0.75,
|
||||||
shapevol: 1,
|
shapevol: 1,
|
||||||
distortvol: 1,
|
distortvol: 1,
|
||||||
|
distorttype: 0,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
byteBeatExpression: '0',
|
byteBeatExpression: '0',
|
||||||
delayfeedback: 0.5,
|
delayfeedback: 0.5,
|
||||||
@@ -193,30 +202,17 @@ export function setVersionDefaults(version) {
|
|||||||
|
|
||||||
export const resetLoadedSounds = () => soundMap.set({});
|
export const resetLoadedSounds = () => soundMap.set({});
|
||||||
|
|
||||||
let audioContext;
|
let externalWorklets = [];
|
||||||
|
export function registerWorklet(url) {
|
||||||
export const setDefaultAudioContext = () => {
|
externalWorklets.push(url);
|
||||||
audioContext = new AudioContext();
|
|
||||||
return audioContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAudioContext = () => {
|
|
||||||
if (!audioContext) {
|
|
||||||
return setDefaultAudioContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
return audioContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getAudioContextCurrentTime() {
|
|
||||||
return getAudioContext().currentTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let workletsLoading;
|
let workletsLoading;
|
||||||
function loadWorklets() {
|
function loadWorklets() {
|
||||||
if (!workletsLoading) {
|
if (!workletsLoading) {
|
||||||
const audioCtx = getAudioContext();
|
const audioCtx = getAudioContext();
|
||||||
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
|
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
||||||
|
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
@@ -282,88 +278,16 @@ export async function initAudioOnFirstClick(options) {
|
|||||||
return audioReady;
|
return audioReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxfeedback = 0.98;
|
let controller;
|
||||||
|
function getSuperdoughAudioController() {
|
||||||
let channelMerger, destinationGain;
|
if (controller == null) {
|
||||||
//update the output channel configuration to match user's audio device
|
controller = new SuperdoughAudioController(getAudioContext());
|
||||||
export function initializeAudioOutput() {
|
}
|
||||||
const audioContext = getAudioContext();
|
return controller;
|
||||||
const maxChannelCount = audioContext.destination.maxChannelCount;
|
|
||||||
audioContext.destination.channelCount = maxChannelCount;
|
|
||||||
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
|
|
||||||
destinationGain = new GainNode(audioContext);
|
|
||||||
channelMerger.connect(destinationGain);
|
|
||||||
destinationGain.connect(audioContext.destination);
|
|
||||||
}
|
}
|
||||||
|
export function connectToDestination(input, channels) {
|
||||||
// input: AudioNode, channels: ?Array<int>
|
const controller = getSuperdoughAudioController();
|
||||||
export const connectToDestination = (input, channels = [0, 1]) => {
|
controller.output.connectToDestination(input, channels);
|
||||||
const ctx = getAudioContext();
|
|
||||||
if (channelMerger == null) {
|
|
||||||
initializeAudioOutput();
|
|
||||||
}
|
|
||||||
//This upmix can be removed if correct channel counts are set throughout the app,
|
|
||||||
// and then strudel could theoretically support surround sound audio files
|
|
||||||
const stereoMix = new StereoPannerNode(ctx);
|
|
||||||
input.connect(stereoMix);
|
|
||||||
|
|
||||||
const splitter = new ChannelSplitterNode(ctx, {
|
|
||||||
numberOfOutputs: stereoMix.channelCount,
|
|
||||||
});
|
|
||||||
stereoMix.connect(splitter);
|
|
||||||
channels.forEach((ch, i) => {
|
|
||||||
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const panic = () => {
|
|
||||||
if (destinationGain == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
|
||||||
destinationGain = null;
|
|
||||||
channelMerger == null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
|
||||||
if (delayfeedback > maxfeedback) {
|
|
||||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
|
||||||
}
|
|
||||||
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
|
||||||
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..
|
|
||||||
connectToOrbit(dly, orbit);
|
|
||||||
orbits[orbit].delayNode = dly;
|
|
||||||
}
|
|
||||||
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, begin, end, properties = {}) {
|
|
||||||
const { shape = 0, ...props } = properties;
|
|
||||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
|
||||||
const lfoprops = {
|
|
||||||
frequency: 1,
|
|
||||||
depth,
|
|
||||||
skew: 0.5,
|
|
||||||
phaseoffset: 0,
|
|
||||||
time: begin,
|
|
||||||
begin,
|
|
||||||
end,
|
|
||||||
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) {
|
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||||
@@ -397,78 +321,6 @@ function getFilterType(ftype) {
|
|||||||
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
|
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
|
||||||
}
|
}
|
||||||
|
|
||||||
//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, channelCount: 2, channelCountMode: 'explicit' }),
|
|
||||||
};
|
|
||||||
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, irspeed, irbegin) {
|
|
||||||
// If no reverb has been created for a given orbit, create one
|
|
||||||
if (!orbits[orbit].reverbNode) {
|
|
||||||
const ac = getAudioContext();
|
|
||||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
|
||||||
connectToOrbit(reverb, orbit);
|
|
||||||
orbits[orbit].reverbNode = reverb;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
|
|
||||||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
|
|
||||||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
|
|
||||||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
|
|
||||||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
|
|
||||||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
|
|
||||||
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
|
|
||||||
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
|
||||||
}
|
|
||||||
return orbits[orbit].reverbNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let analysers = {},
|
export let analysers = {},
|
||||||
analysersData = {};
|
analysersData = {};
|
||||||
|
|
||||||
@@ -501,15 +353,8 @@ export function getAnalyzerData(type = 'time', id = 1) {
|
|||||||
return analysersData[id];
|
return analysersData[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectSend(input, effect, wet) {
|
|
||||||
const send = gainNode(wet);
|
|
||||||
input.connect(send);
|
|
||||||
send.connect(effect);
|
|
||||||
return send;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetGlobalEffects() {
|
export function resetGlobalEffects() {
|
||||||
orbits = {};
|
controller?.reset();
|
||||||
analysers = {};
|
analysers = {};
|
||||||
analysersData = {};
|
analysersData = {};
|
||||||
}
|
}
|
||||||
@@ -524,6 +369,7 @@ function mapChannelNumbers(channels) {
|
|||||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
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
|
// new: t is always expected to be the absolute target onset time
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
|
const audioController = getSuperdoughAudioController();
|
||||||
|
|
||||||
let { stretch } = value;
|
let { stretch } = value;
|
||||||
if (stretch != null) {
|
if (stretch != null) {
|
||||||
@@ -563,8 +409,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
postgain = getDefaultValue('postgain'),
|
postgain = getDefaultValue('postgain'),
|
||||||
density = getDefaultValue('density'),
|
density = getDefaultValue('density'),
|
||||||
duckorbit,
|
duckorbit,
|
||||||
|
duckonset,
|
||||||
duckattack,
|
duckattack,
|
||||||
duckdepth,
|
duckdepth,
|
||||||
|
djf,
|
||||||
// filters
|
// filters
|
||||||
fanchor = getDefaultValue('fanchor'),
|
fanchor = getDefaultValue('fanchor'),
|
||||||
drive = 0.69,
|
drive = 0.69,
|
||||||
@@ -608,6 +456,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
shapevol = getDefaultValue('shapevol'),
|
shapevol = getDefaultValue('shapevol'),
|
||||||
distort,
|
distort,
|
||||||
distortvol = getDefaultValue('distortvol'),
|
distortvol = getDefaultValue('distortvol'),
|
||||||
|
distorttype = getDefaultValue('distorttype'),
|
||||||
pan,
|
pan,
|
||||||
vowel,
|
vowel,
|
||||||
delay = getDefaultValue('delay'),
|
delay = getDefaultValue('delay'),
|
||||||
@@ -641,10 +490,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
);
|
);
|
||||||
|
|
||||||
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
|
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
|
||||||
setOrbit(ac, orbit, channels, t, cycle, cps);
|
const orbitBus = audioController.getOrbit(orbit, channels);
|
||||||
|
|
||||||
if (duckorbit != null) {
|
if (duckorbit != null) {
|
||||||
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
|
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
gain = applyGainCurve(nanFallback(gain, 1));
|
gain = applyGainCurve(nanFallback(gain, 1));
|
||||||
@@ -691,7 +539,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
audioNodes.forEach((n) => n?.disconnect());
|
audioNodes.forEach((n) => n?.disconnect());
|
||||||
activeSoundSources.delete(chainID);
|
activeSoundSources.delete(chainID);
|
||||||
};
|
};
|
||||||
const soundHandle = await onTrigger(t, value, onEnded);
|
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||||
|
|
||||||
if (soundHandle) {
|
if (soundHandle) {
|
||||||
sourceNode = soundHandle.node;
|
sourceNode = soundHandle.node;
|
||||||
@@ -783,12 +631,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||||
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||||
|
|
||||||
if (tremolosync != null) {
|
if (tremolosync != null) {
|
||||||
tremolo = cps * tremolosync;
|
tremolo = cps * tremolosync;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value.wtPosSynced != null) {
|
||||||
|
value.wtPosRate /= cps;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.wtWarpSynced != null) {
|
||||||
|
value.wtWarpRate /= cps;
|
||||||
|
}
|
||||||
|
|
||||||
if (tremolo !== undefined) {
|
if (tremolo !== undefined) {
|
||||||
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
// 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
|
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
||||||
@@ -834,14 +690,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
chain.push(post);
|
chain.push(post);
|
||||||
|
|
||||||
// delay
|
// delay
|
||||||
let delaySend;
|
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
|
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||||
delaySend = effectSend(post, delayNode, delay);
|
orbitBus.sendDelay(post, delay);
|
||||||
audioNodes.push(delaySend);
|
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
let reverbSend;
|
|
||||||
if (room > 0) {
|
if (room > 0) {
|
||||||
let roomIR;
|
let roomIR;
|
||||||
if (ir !== undefined) {
|
if (ir !== undefined) {
|
||||||
@@ -854,25 +707,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||||
}
|
}
|
||||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||||
reverbSend = effectSend(post, reverbNode, room);
|
orbitBus.sendReverb(post, room);
|
||||||
audioNodes.push(reverbSend);
|
}
|
||||||
|
|
||||||
|
if (djf != null) {
|
||||||
|
orbitBus.getDjf(djf, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// analyser
|
// analyser
|
||||||
let analyserSend;
|
|
||||||
if (analyze) {
|
if (analyze) {
|
||||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||||
analyserSend = effectSend(post, analyserNode, 1);
|
const analyserSend = effectSend(post, analyserNode, 1);
|
||||||
audioNodes.push(analyserSend);
|
audioNodes.push(analyserSend);
|
||||||
}
|
}
|
||||||
if (dry != null) {
|
if (dry != null) {
|
||||||
dry = applyGainCurve(dry);
|
dry = applyGainCurve(dry);
|
||||||
const dryGain = new GainNode(ac, { gain: dry });
|
const dryGain = new GainNode(ac, { gain: dry });
|
||||||
chain.push(dryGain);
|
chain.push(dryGain);
|
||||||
connectToOrbit(dryGain, orbit);
|
orbitBus.connectToOutput(dryGain);
|
||||||
} else {
|
} else {
|
||||||
connectToOrbit(post, orbit);
|
orbitBus.connectToOutput(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect chain elements together
|
// connect chain elements together
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||||
|
import { errorLogger } from './logger.mjs';
|
||||||
|
import { clamp } from './util.mjs';
|
||||||
|
|
||||||
|
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||||
|
|
||||||
|
export class Orbit {
|
||||||
|
reverbNode;
|
||||||
|
delayNode;
|
||||||
|
output;
|
||||||
|
summingNode;
|
||||||
|
djfNode;
|
||||||
|
audioContext;
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
|
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
|
this.summingNode.connect(this.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.output.disconnect();
|
||||||
|
this.summingNode.disconnect();
|
||||||
|
this.delayNode?.disconnect();
|
||||||
|
this.reverbNode?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDjf(value, t = 0) {
|
||||||
|
if (this.djfNode == null) {
|
||||||
|
this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value });
|
||||||
|
this.summingNode.disconnect();
|
||||||
|
this.summingNode.connect(this.djfNode);
|
||||||
|
this.djfNode.connect(this.output);
|
||||||
|
}
|
||||||
|
const val = this.djfNode.parameters.get('value');
|
||||||
|
val.setValueAtTime(value, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDelay(delaytime = 0, feedback = 0.5, t) {
|
||||||
|
const maxfeedback = 0.98;
|
||||||
|
if (feedback > maxfeedback) {
|
||||||
|
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
|
||||||
|
}
|
||||||
|
feedback = clamp(feedback, 0, 0.98);
|
||||||
|
if (this.delayNode == null) {
|
||||||
|
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback);
|
||||||
|
this.delayNode.connect(this.summingNode);
|
||||||
|
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||||
|
}
|
||||||
|
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
|
||||||
|
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
|
||||||
|
return this.delayNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||||
|
// If no reverb has been created for a given orbit, create one
|
||||||
|
if (this.reverbNode == null) {
|
||||||
|
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
|
this.reverbNode.connect(this.summingNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasChanged(duration, this.reverbNode.duration) ||
|
||||||
|
hasChanged(fade, this.reverbNode.fade) ||
|
||||||
|
hasChanged(lp, this.reverbNode.lp) ||
|
||||||
|
hasChanged(dim, this.reverbNode.dim) ||
|
||||||
|
hasChanged(irspeed, this.reverbNode.irspeed) ||
|
||||||
|
hasChanged(irbegin, this.reverbNode.irbegin) ||
|
||||||
|
this.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
|
||||||
|
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
|
}
|
||||||
|
return this.reverbNode;
|
||||||
|
}
|
||||||
|
sendReverb(node, amount) {
|
||||||
|
effectSend(node, this.reverbNode, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDelay(node, amount) {
|
||||||
|
effectSend(node, this.delayNode, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||||
|
const onset = onsettime;
|
||||||
|
const attack = Math.max(attacktime, 0.002);
|
||||||
|
const gainParam = this.output.gain;
|
||||||
|
webAudioTimeout(
|
||||||
|
this.audioContext,
|
||||||
|
() => {
|
||||||
|
const now = this.audioContext.currentTime;
|
||||||
|
|
||||||
|
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
|
||||||
|
// on browsers which lack that method
|
||||||
|
const currVal = gainParam.value;
|
||||||
|
gainParam.cancelScheduledValues(now);
|
||||||
|
gainParam.setValueAtTime(currVal, now);
|
||||||
|
|
||||||
|
const t0 = Math.max(t, now); // guard against now > t
|
||||||
|
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
|
||||||
|
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
|
||||||
|
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
t - 0.01,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToOutput(node) {
|
||||||
|
node.connect(this.summingNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuperdoughOutput {
|
||||||
|
channelMerger;
|
||||||
|
destinationGain;
|
||||||
|
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
this.initializeAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeAudio() {
|
||||||
|
const audioContext = this.audioContext;
|
||||||
|
const maxChannelCount = audioContext.destination.maxChannelCount;
|
||||||
|
this.audioContext.destination.channelCount = maxChannelCount;
|
||||||
|
this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
|
||||||
|
this.destinationGain = new GainNode(audioContext);
|
||||||
|
this.channelMerger.connect(this.destinationGain);
|
||||||
|
this.destinationGain.connect(audioContext.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.disconnect();
|
||||||
|
this.initializeAudio();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
this.channelMerger.disconnect();
|
||||||
|
this.destinationGain.disconnect();
|
||||||
|
this.destinationGain = null;
|
||||||
|
this.channelMerger = null;
|
||||||
|
}
|
||||||
|
connectToDestination = (input, channels = [0, 1]) => {
|
||||||
|
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||||
|
// and then strudel could theoretically support surround sound audio files
|
||||||
|
const stereoMix = new StereoPannerNode(this.audioContext);
|
||||||
|
input.connect(stereoMix);
|
||||||
|
|
||||||
|
const splitter = new ChannelSplitterNode(this.audioContext, {
|
||||||
|
numberOfOutputs: stereoMix.channelCount,
|
||||||
|
});
|
||||||
|
stereoMix.connect(splitter);
|
||||||
|
channels.forEach((ch, i) => {
|
||||||
|
splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuperdoughAudioController {
|
||||||
|
audioContext;
|
||||||
|
output;
|
||||||
|
nodes = {};
|
||||||
|
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
this.output = new SuperdoughOutput(audioContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
Array.from(this.nodes).forEach((node) => {
|
||||||
|
node.disconnect();
|
||||||
|
});
|
||||||
|
this.nodes = {};
|
||||||
|
this.output.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||||
|
const targetArr = [targetOrbits].flat();
|
||||||
|
const onsetArr = [onsettime].flat();
|
||||||
|
const attackArr = [attacktime].flat();
|
||||||
|
const depthArr = [depth].flat();
|
||||||
|
|
||||||
|
targetArr.forEach((target, idx) => {
|
||||||
|
const orbit = this.nodes[target];
|
||||||
|
|
||||||
|
if (orbit == null) {
|
||||||
|
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onset = onsetArr[idx] ?? onsetArr[0];
|
||||||
|
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
|
||||||
|
const depth = depthArr[idx] ?? depthArr[0];
|
||||||
|
|
||||||
|
orbit.duck(t, onset, attack, depth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrbit(orbitNum, channels) {
|
||||||
|
if (this.nodes[orbitNum] == null) {
|
||||||
|
this.nodes[orbitNum] = new Orbit(this.audioContext);
|
||||||
|
this.output.connectToDestination(this.nodes[orbitNum].output, channels);
|
||||||
|
}
|
||||||
|
return this.nodes[orbitNum];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,22 @@
|
|||||||
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
|
import { registerSound, soundMap } from './superdough.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
|
destroyAudioWorkletNode,
|
||||||
gainNode,
|
gainNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
|
getFrequencyFromValue,
|
||||||
|
getLfo,
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
webAudioTimeout,
|
|
||||||
getWorklet,
|
getWorklet,
|
||||||
noises,
|
noises,
|
||||||
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
|
||||||
const getFrequencyFromValue = (value, defaultNote = 36) => {
|
|
||||||
let { note, freq } = value;
|
|
||||||
note = note || defaultNote;
|
|
||||||
if (typeof note === 'string') {
|
|
||||||
note = noteToMidi(note); // e.g. c3 => 48
|
|
||||||
}
|
|
||||||
// get frequency
|
|
||||||
if (!freq && typeof note === 'number') {
|
|
||||||
freq = midiToFreq(note); // + 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Number(freq);
|
|
||||||
};
|
|
||||||
function destroyAudioWorkletNode(node) {
|
|
||||||
if (node == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.disconnect();
|
|
||||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
['tri', 'triangle'],
|
['tri', 'triangle'],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
|
|||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -16,13 +16,17 @@ export const tokenizeNote = (note) => {
|
|||||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||||
|
|
||||||
|
export const getAccidentalsOffset = (accidentals) => {
|
||||||
|
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = chromas[pc.toLowerCase()];
|
const chroma = chromas[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
const offset = getAccidentalsOffset(acc);
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
@@ -76,3 +80,32 @@ export function cycleToSeconds(cycle, cps) {
|
|||||||
export function secondsToCycle(t, cps) {
|
export function secondsToCycle(t, cps) {
|
||||||
return t * cps;
|
return t * cps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
|
const { s, n = 0 } = hapValue;
|
||||||
|
let midi = valueToMidi(hapValue, 36);
|
||||||
|
let transpose = midi - 36; // C3 is middle C;
|
||||||
|
let url;
|
||||||
|
let index = 0;
|
||||||
|
if (Array.isArray(bank)) {
|
||||||
|
index = getSoundIndex(n, bank.length);
|
||||||
|
url = bank[index];
|
||||||
|
} else {
|
||||||
|
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||||
|
// object format will expect keys as notes
|
||||||
|
const closest = Object.keys(bank)
|
||||||
|
.filter((k) => !k.startsWith('_'))
|
||||||
|
.reduce(
|
||||||
|
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
|
url = bank[closest][index];
|
||||||
|
}
|
||||||
|
const label = `${s}:${index}`;
|
||||||
|
return { transpose, url, index, midi, label };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
|
import { getCommonSampleInfo } from './util.mjs';
|
||||||
|
import {
|
||||||
|
applyFM,
|
||||||
|
applyParameterModulators,
|
||||||
|
destroyAudioWorkletNode,
|
||||||
|
getADSRValues,
|
||||||
|
getFrequencyFromValue,
|
||||||
|
getParamADSR,
|
||||||
|
getPitchEnvelope,
|
||||||
|
getVibratoOscillator,
|
||||||
|
getWorklet,
|
||||||
|
webAudioTimeout,
|
||||||
|
} from './helpers.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
export const Warpmode = Object.freeze({
|
||||||
|
NONE: 0,
|
||||||
|
ASYM: 1,
|
||||||
|
MIRROR: 2,
|
||||||
|
BENDP: 3,
|
||||||
|
BENDM: 4,
|
||||||
|
BENDMP: 5,
|
||||||
|
SYNC: 6,
|
||||||
|
QUANT: 7,
|
||||||
|
FOLD: 8,
|
||||||
|
PWM: 9,
|
||||||
|
ORBIT: 10,
|
||||||
|
SPIN: 11,
|
||||||
|
CHAOS: 12,
|
||||||
|
PRIMES: 13,
|
||||||
|
BINARY: 14,
|
||||||
|
BROWNIAN: 15,
|
||||||
|
RECIPROCAL: 16,
|
||||||
|
WORMHOLE: 17,
|
||||||
|
LOGISTIC: 18,
|
||||||
|
SIGMOID: 19,
|
||||||
|
FRACTAL: 20,
|
||||||
|
FLIP: 21,
|
||||||
|
});
|
||||||
|
|
||||||
|
const seenKeys = new Set();
|
||||||
|
async function getPayload(url, label, frameLen = 2048) {
|
||||||
|
const key = `${url},${frameLen}`;
|
||||||
|
if (!seenKeys.has(key)) {
|
||||||
|
const buf = await loadBuffer(url, label);
|
||||||
|
const ch0 = buf.getChannelData(0);
|
||||||
|
const total = ch0.length;
|
||||||
|
const numFrames = Math.max(1, Math.floor(total / frameLen));
|
||||||
|
const frames = new Array(numFrames);
|
||||||
|
for (let i = 0; i < numFrames; i++) {
|
||||||
|
const start = i * frameLen;
|
||||||
|
frames[i] = ch0.subarray(start, start + frameLen);
|
||||||
|
}
|
||||||
|
seenKeys.add(key);
|
||||||
|
return { frames, frameLen, numFrames, key };
|
||||||
|
}
|
||||||
|
return { frameLen, key }; // worklet will use the cached version
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanFileSize(bytes, si) {
|
||||||
|
var thresh = si ? 1000 : 1024;
|
||||||
|
if (bytes < thresh) return bytes + ' B';
|
||||||
|
var units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
var u = -1;
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (bytes >= thresh);
|
||||||
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the sample rate of a .wav file
|
||||||
|
function parseWavSampleRate(arrBuf) {
|
||||||
|
const dv = new DataView(arrBuf);
|
||||||
|
// Header is "RIFF<chunk size (4 bytes)>WAVE", so 12 bytes
|
||||||
|
let p = 12;
|
||||||
|
// Look through chunks for the format header
|
||||||
|
// (they will always have an 8 byte header (id and size) followed by a payload)
|
||||||
|
while (p + 8 <= dv.byteLength) {
|
||||||
|
// Parse id
|
||||||
|
const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3));
|
||||||
|
// Parse chunk size
|
||||||
|
const size = dv.getUint32(p + 4, true);
|
||||||
|
if (id === 'fmt ') {
|
||||||
|
// The format chunk contains the sample rate after
|
||||||
|
// 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels
|
||||||
|
// (for a total of 12)
|
||||||
|
return dv.getUint32(p + 12, true);
|
||||||
|
}
|
||||||
|
// Advance to next chunk
|
||||||
|
p += 8 + size + (size & 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodeAtNativeRate(arr) {
|
||||||
|
const sr = parseWavSampleRate(arr) || 44100;
|
||||||
|
const tempAC = new OfflineAudioContext(1, 1, sr);
|
||||||
|
return await tempAC.decodeAudioData(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCache = {};
|
||||||
|
const loadBuffer = (url, label) => {
|
||||||
|
url = url.replace('#', '%23');
|
||||||
|
if (!loadCache[url]) {
|
||||||
|
logger(`[wavetable] load table ${label}..`, 'load-table', { url });
|
||||||
|
const timestamp = Date.now();
|
||||||
|
loadCache[url] = fetch(url)
|
||||||
|
.then((res) => res.arrayBuffer())
|
||||||
|
.then(async (res) => {
|
||||||
|
const took = Date.now() - timestamp;
|
||||||
|
const size = humanFileSize(res.byteLength);
|
||||||
|
logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url });
|
||||||
|
const decoded = await decodeAtNativeRate(res);
|
||||||
|
return decoded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return loadCache[url];
|
||||||
|
};
|
||||||
|
|
||||||
|
function githubPath(base, subpath = '') {
|
||||||
|
if (!base.startsWith('github:')) {
|
||||||
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
|
}
|
||||||
|
let [_, path] = base.split('github:');
|
||||||
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
if (path.split('/').length === 2) {
|
||||||
|
// assume main as default branch if none set
|
||||||
|
path += '/main';
|
||||||
|
}
|
||||||
|
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _processTables = (json, baseUrl, frameLen, options = {}) => {
|
||||||
|
baseUrl = json._base || baseUrl;
|
||||||
|
return Object.entries(json).forEach(([key, tables]) => {
|
||||||
|
if (key === '_base') return false;
|
||||||
|
if (typeof tables === 'string') {
|
||||||
|
tables = [tables];
|
||||||
|
}
|
||||||
|
if (typeof tables !== 'object') {
|
||||||
|
throw new Error('wrong json format for ' + key);
|
||||||
|
}
|
||||||
|
let resolvedUrl = baseUrl;
|
||||||
|
if (resolvedUrl.startsWith('github:')) {
|
||||||
|
resolvedUrl = githubPath(resolvedUrl, '');
|
||||||
|
}
|
||||||
|
tables = tables
|
||||||
|
.map((t) => resolvedUrl + t)
|
||||||
|
.filter((t) => {
|
||||||
|
if (!t.toLowerCase().endsWith('.wav')) {
|
||||||
|
logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (tables.length) {
|
||||||
|
registerWaveTable(key, tables, { baseUrl, frameLen });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function registerWaveTable(key, tables, params) {
|
||||||
|
registerSound(
|
||||||
|
key,
|
||||||
|
(t, hapValue, onended, cps) => {
|
||||||
|
return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'wavetable',
|
||||||
|
tables,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a collection of wavetables to use with `s`
|
||||||
|
*
|
||||||
|
* @name tables
|
||||||
|
*/
|
||||||
|
export const tables = async (url, frameLen, json, options = {}) => {
|
||||||
|
if (json !== undefined) return _processTables(json, url, frameLen);
|
||||||
|
if (url.startsWith('github:')) {
|
||||||
|
url = githubPath(url, 'strudel.json');
|
||||||
|
}
|
||||||
|
if (url.startsWith('local:')) {
|
||||||
|
url = `http://localhost:5432`;
|
||||||
|
}
|
||||||
|
if (typeof fetch !== 'function') {
|
||||||
|
// not a browser
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
// skip fetch when in node / testing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return fetch(url)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((json) => _processTables(json, url, frameLen, options))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`error loading "${url}"`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||||
|
const { s, n = 0, duration, clip } = value;
|
||||||
|
const ac = getAudioContext();
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||||
|
let { warpmode } = value;
|
||||||
|
if (typeof warpmode === 'string') {
|
||||||
|
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
||||||
|
}
|
||||||
|
const frequency = getFrequencyFromValue(value);
|
||||||
|
const { url, label } = getCommonSampleInfo(value, tables);
|
||||||
|
const payload = await getPayload(url, label, frameLen);
|
||||||
|
let holdEnd = t + duration;
|
||||||
|
if (clip !== undefined) {
|
||||||
|
holdEnd = Math.min(t + clip * duration, holdEnd);
|
||||||
|
}
|
||||||
|
const endWithRelease = holdEnd + release;
|
||||||
|
const envEnd = endWithRelease + 0.01;
|
||||||
|
const source = getWorklet(
|
||||||
|
ac,
|
||||||
|
'wavetable-oscillator-processor',
|
||||||
|
{
|
||||||
|
begin: t,
|
||||||
|
end: envEnd,
|
||||||
|
frequency,
|
||||||
|
freqspread: value.detune,
|
||||||
|
position: value.wt,
|
||||||
|
warp: value.warp,
|
||||||
|
warpMode: warpmode,
|
||||||
|
voices: Math.max(value.unison ?? 1, 1),
|
||||||
|
panspread: value.spread,
|
||||||
|
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||||
|
},
|
||||||
|
{ outputChannelCount: [2] },
|
||||||
|
);
|
||||||
|
source.port.postMessage({ type: 'table', payload });
|
||||||
|
if (ac.currentTime > t) {
|
||||||
|
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
|
||||||
|
const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease];
|
||||||
|
const wtParams = source.parameters;
|
||||||
|
const positionParam = wtParams.get('position');
|
||||||
|
const warpParam = wtParams.get('warp');
|
||||||
|
|
||||||
|
let wtrate = value.wtrate;
|
||||||
|
if (value.wtsync != null) {
|
||||||
|
wtrate = cps * value.wtsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wtPosModulators = applyParameterModulators(
|
||||||
|
ac,
|
||||||
|
positionParam,
|
||||||
|
t,
|
||||||
|
endWithRelease,
|
||||||
|
{
|
||||||
|
offset: value.wt,
|
||||||
|
amount: value.wtenv,
|
||||||
|
defaultAmount: 0.5,
|
||||||
|
shape: 'linear',
|
||||||
|
values: posADSRParams,
|
||||||
|
holdEnd,
|
||||||
|
defaultValues: [0, 0.5, 0, 0.1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequency: wtrate,
|
||||||
|
depth: value.wtdepth,
|
||||||
|
defaultDepth: 0.5,
|
||||||
|
shape: value.wtshape,
|
||||||
|
skew: value.wtskew,
|
||||||
|
dcoffset: value.wtdc ?? 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let warprate = value.warprate;
|
||||||
|
if (value.warpsync != null) {
|
||||||
|
warprate = warprate = cps * value.warpsync;
|
||||||
|
}
|
||||||
|
const wtWarpModulators = applyParameterModulators(
|
||||||
|
ac,
|
||||||
|
warpParam,
|
||||||
|
t,
|
||||||
|
endWithRelease,
|
||||||
|
{
|
||||||
|
offset: value.warp,
|
||||||
|
amount: value.warpenv,
|
||||||
|
defaultAmount: 0.5,
|
||||||
|
shape: 'linear',
|
||||||
|
values: warpADSRParams,
|
||||||
|
holdEnd,
|
||||||
|
defaultValues: [0, 0.5, 0, 0.1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequency: warprate,
|
||||||
|
depth: value.warpdepth,
|
||||||
|
defaultDepth: 0.5,
|
||||||
|
shape: value.warpshape,
|
||||||
|
skew: value.warpskew,
|
||||||
|
dcoffset: value.warpdc ?? 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||||
|
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
||||||
|
const envGain = ac.createGain();
|
||||||
|
const node = source.connect(envGain);
|
||||||
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||||
|
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||||
|
const handle = { node, source };
|
||||||
|
const timeoutNode = webAudioTimeout(
|
||||||
|
ac,
|
||||||
|
() => {
|
||||||
|
destroyAudioWorkletNode(source);
|
||||||
|
vibratoOscillator?.stop();
|
||||||
|
fm?.stop();
|
||||||
|
node.disconnect();
|
||||||
|
wtPosModulators?.disconnect();
|
||||||
|
wtWarpModulators?.disconnect();
|
||||||
|
onended();
|
||||||
|
},
|
||||||
|
t,
|
||||||
|
envEnd,
|
||||||
|
);
|
||||||
|
handle.stop = (time) => {
|
||||||
|
timeoutNode.stop(time);
|
||||||
|
};
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
@@ -4,9 +4,24 @@
|
|||||||
|
|
||||||
import OLAProcessor from './ola-processor';
|
import OLAProcessor from './ola-processor';
|
||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
|
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
const _mod = (n, m) => ((n % m) + m) % m;
|
const mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
const lerp = (a, b, n) => n * (b - a) + a;
|
||||||
|
const pv = (arr, n) => arr[n] ?? arr[0];
|
||||||
|
const frac = (x) => x - Math.floor(x);
|
||||||
|
const ffloor = (x) => x | 0; // fast floor for non-negative
|
||||||
|
|
||||||
|
const getUnisonDetune = (unison, detune, voiceIndex) => {
|
||||||
|
if (unison < 2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
||||||
|
};
|
||||||
|
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
||||||
|
return frequency * Math.pow(2, detune / 12);
|
||||||
|
};
|
||||||
|
|
||||||
// Restrict phase to the range [0, maxPhase) via wrapping
|
// Restrict phase to the range [0, maxPhase) via wrapping
|
||||||
function wrapPhase(phase, maxPhase = 1) {
|
function wrapPhase(phase, maxPhase = 1) {
|
||||||
@@ -123,7 +138,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
process(_inputs, outputs, parameters) {
|
||||||
const begin = parameters['begin'][0];
|
const begin = parameters['begin'][0];
|
||||||
if (currentTime >= parameters.end[0]) {
|
if (currentTime >= parameters.end[0]) {
|
||||||
return false;
|
return false;
|
||||||
@@ -150,7 +165,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
const blockSize = output[0].length ?? 0;
|
const blockSize = output[0].length ?? 0;
|
||||||
|
|
||||||
if (this.phase == null) {
|
if (this.phase == null) {
|
||||||
this.phase = _mod(time * frequency + phaseoffset, 1);
|
this.phase = mod(time * frequency + phaseoffset, 1);
|
||||||
}
|
}
|
||||||
const dt = frequency / sampleRate;
|
const dt = frequency / sampleRate;
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
@@ -271,6 +286,73 @@ class ShapeProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('shape-processor', ShapeProcessor);
|
registerProcessor('shape-processor', ShapeProcessor);
|
||||||
|
|
||||||
|
class TwoPoleFilter {
|
||||||
|
s0 = 0;
|
||||||
|
s1 = 0;
|
||||||
|
update(s, cutoff, resonance = 0) {
|
||||||
|
// Out of bound values can produce NaNs
|
||||||
|
resonance = clamp(resonance, 0, 1);
|
||||||
|
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
|
||||||
|
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
|
||||||
|
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
||||||
|
const mrc = 1 - r * c;
|
||||||
|
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
|
||||||
|
this.s1 = mrc * this.s1 + c * this.s0; // lpf
|
||||||
|
return this.s1; // return lpf by default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DJFProcessor extends AudioWorkletProcessor {
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [{ name: 'value', defaultValue: 0.5 }];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.filters = [new TwoPoleFilter(), new TwoPoleFilter()];
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters) {
|
||||||
|
const input = inputs[0];
|
||||||
|
const output = outputs[0];
|
||||||
|
|
||||||
|
const hasInput = !(input[0] === undefined);
|
||||||
|
this.started = hasInput;
|
||||||
|
|
||||||
|
const value = clamp(parameters.value[0], 0, 1);
|
||||||
|
let filterType = 'none';
|
||||||
|
let cutoff;
|
||||||
|
let v = 1;
|
||||||
|
if (value > 0.51) {
|
||||||
|
filterType = 'hipass';
|
||||||
|
v = (value - 0.5) * 2;
|
||||||
|
} else if (value < 0.49) {
|
||||||
|
filterType = 'lopass';
|
||||||
|
v = value * 2;
|
||||||
|
}
|
||||||
|
cutoff = Math.pow(v * 11, 4);
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
for (let n = 0; n < blockSize; n++) {
|
||||||
|
if (filterType == 'none') {
|
||||||
|
output[i][n] = input[i][n];
|
||||||
|
} else {
|
||||||
|
this.filters[i].update(input[i][n], cutoff, 0.1);
|
||||||
|
if (filterType === 'lopass') {
|
||||||
|
output[i][n] = this.filters[i].s1;
|
||||||
|
} else if (filterType === 'hipass') {
|
||||||
|
output[i][n] = input[i][n] - this.filters[i].s1;
|
||||||
|
} else {
|
||||||
|
output[i][n] = input[i][n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProcessor('djf-processor', DJFProcessor);
|
||||||
|
|
||||||
function fast_tanh(x) {
|
function fast_tanh(x) {
|
||||||
const x2 = x * x;
|
const x2 = x * x;
|
||||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
||||||
@@ -349,9 +431,10 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor({ processorOptions }) {
|
||||||
super();
|
super();
|
||||||
this.started = false;
|
this.started = false;
|
||||||
|
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
process(inputs, outputs, parameters) {
|
||||||
@@ -363,13 +446,12 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.started = hasInput;
|
this.started = hasInput;
|
||||||
|
|
||||||
const shape = Math.expm1(parameters.distort[0]);
|
|
||||||
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
|
|
||||||
|
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
for (let i = 0; i < input.length; i++) {
|
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
||||||
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
|
const shape = Math.expm1(pv(parameters.distort, n));
|
||||||
|
for (let ch = 0; ch < input.length; ch++) {
|
||||||
|
const x = input[ch][n];
|
||||||
|
output[ch][n] = postgain * this.algorithm(x, shape);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -378,21 +460,6 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
registerProcessor('distort-processor', DistortProcessor);
|
registerProcessor('distort-processor', DistortProcessor);
|
||||||
|
|
||||||
// SUPERSAW
|
// SUPERSAW
|
||||||
function lerp(a, b, n) {
|
|
||||||
return n * (b - a) + a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUnisonDetune(unison, detune, voiceIndex) {
|
|
||||||
if (unison < 2) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySemitoneDetuneToFrequency(frequency, detune) {
|
|
||||||
return frequency * Math.pow(2, detune / 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -444,7 +511,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
process(input, outputs, params) {
|
process(_input, outputs, params) {
|
||||||
if (currentTime <= params.begin[0]) {
|
if (currentTime <= params.begin[0]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -454,29 +521,31 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const voices = params.voices[0];
|
|
||||||
const freqspread = params.freqspread[0];
|
|
||||||
const panspread = params.panspread[0] * 0.5 + 0.5;
|
|
||||||
const gain1 = Math.sqrt(1 - panspread);
|
|
||||||
const gain2 = Math.sqrt(panspread);
|
|
||||||
|
|
||||||
for (let n = 0; n < voices; n++) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
const isOdd = (n & 1) == 1;
|
const detune = pv(params.detune, i);
|
||||||
let gainL = gain1;
|
const voices = pv(params.voices, i);
|
||||||
let gainR = gain2;
|
const freqspread = pv(params.freqspread, i);
|
||||||
// invert right and left gain
|
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
||||||
if (isOdd) {
|
const gain1 = Math.sqrt(1 - panspread);
|
||||||
gainL = gain2;
|
const gain2 = Math.sqrt(panspread);
|
||||||
gainR = gain1;
|
let freq = pv(params.frequency, i);
|
||||||
}
|
// Main detuning
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||||
// Main detuning
|
for (let n = 0; n < voices; n++) {
|
||||||
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
|
const isOdd = (n & 1) == 1;
|
||||||
|
let gainL = gain1;
|
||||||
|
let gainR = gain2;
|
||||||
|
// invert right and left gain
|
||||||
|
if (isOdd) {
|
||||||
|
gainL = gain2;
|
||||||
|
gainR = gain1;
|
||||||
|
}
|
||||||
// Individual voice detuning
|
// Individual voice detuning
|
||||||
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
|
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
|
||||||
// We must wrap this here because it is passed into sawblep below which
|
// We must wrap this here because it is passed into sawblep below which
|
||||||
// has domain [0, 1]
|
// has domain [0, 1]
|
||||||
const dt = _mod(freq / sampleRate, 1);
|
const dt = mod(freqVoice / sampleRate, 1);
|
||||||
this.phase[n] = this.phase[n] ?? Math.random();
|
this.phase[n] = this.phase[n] ?? Math.random();
|
||||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||||
|
|
||||||
@@ -907,3 +976,346 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||||
|
|
||||||
|
export const WarpMode = Object.freeze({
|
||||||
|
NONE: 0,
|
||||||
|
ASYM: 1,
|
||||||
|
MIRROR: 2,
|
||||||
|
BENDP: 3,
|
||||||
|
BENDM: 4,
|
||||||
|
BENDMP: 5,
|
||||||
|
SYNC: 6,
|
||||||
|
QUANT: 7,
|
||||||
|
FOLD: 8,
|
||||||
|
PWM: 9,
|
||||||
|
ORBIT: 10,
|
||||||
|
SPIN: 11,
|
||||||
|
CHAOS: 12,
|
||||||
|
PRIMES: 13,
|
||||||
|
BINARY: 14,
|
||||||
|
BROWNIAN: 15,
|
||||||
|
RECIPROCAL: 16,
|
||||||
|
WORMHOLE: 17,
|
||||||
|
LOGISTIC: 18,
|
||||||
|
SIGMOID: 19,
|
||||||
|
FRACTAL: 20,
|
||||||
|
FLIP: 21,
|
||||||
|
});
|
||||||
|
|
||||||
|
function hash32(u) {
|
||||||
|
u = u + 0x7ed55d16 + (u << 12);
|
||||||
|
u = u ^ 0xc761c23c ^ (u >>> 19);
|
||||||
|
u = u + 0x165667b1 + (u << 5);
|
||||||
|
u = (u + 0xd3a2646c) ^ (u << 9);
|
||||||
|
u = u + 0xfd7046c5 + (u << 3);
|
||||||
|
u = u ^ 0xb55a4f09 ^ (u >>> 16);
|
||||||
|
return u >>> 0;
|
||||||
|
}
|
||||||
|
const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000;
|
||||||
|
|
||||||
|
function bitReverse(i, n) {
|
||||||
|
let r = 0;
|
||||||
|
for (let b = 0; b < n; b++) {
|
||||||
|
r = (r << 1) | (i & 1);
|
||||||
|
i >>>= 1;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function noise(x) {
|
||||||
|
const i = Math.floor(x),
|
||||||
|
f = x - i;
|
||||||
|
const a = hash01(i),
|
||||||
|
b = hash01(i + 1);
|
||||||
|
return a + (b - a) * f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function brownian(x, oct = 4) {
|
||||||
|
let amp = 0.5,
|
||||||
|
sum = 0,
|
||||||
|
norm = 0,
|
||||||
|
freq = 1;
|
||||||
|
for (let o = 0; o < oct; o++) {
|
||||||
|
sum += amp * noise(x * freq);
|
||||||
|
norm += amp;
|
||||||
|
amp *= 0.5;
|
||||||
|
freq *= 2;
|
||||||
|
}
|
||||||
|
return (sum / norm) * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tablesCache = {};
|
||||||
|
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [
|
||||||
|
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
|
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
|
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||||
|
{ name: 'detune', defaultValue: 0 },
|
||||||
|
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||||
|
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||||
|
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||||
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
|
{ name: 'voices', defaultValue: 1, min: 1 },
|
||||||
|
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||||
|
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this.frameLen = 0;
|
||||||
|
this.numFrames = 0;
|
||||||
|
this.phase = [];
|
||||||
|
this.invSR = 1 / sampleRate;
|
||||||
|
|
||||||
|
this.port.onmessage = (e) => {
|
||||||
|
const { type, payload } = e.data || {};
|
||||||
|
if (type === 'table') {
|
||||||
|
const key = payload.key;
|
||||||
|
this.frameLen = payload.frameLen;
|
||||||
|
if (!tablesCache[key]) {
|
||||||
|
const tables = [payload.frames];
|
||||||
|
let table = tables[0];
|
||||||
|
for (let level = 1; level < 1; level++) {
|
||||||
|
const nextLen = table.length >> 1;
|
||||||
|
const nextTable = table.map((frame) => {
|
||||||
|
const avg = new Float32Array(nextLen);
|
||||||
|
for (let i = 0; i < nextLen; i++) {
|
||||||
|
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
|
||||||
|
}
|
||||||
|
return avg;
|
||||||
|
});
|
||||||
|
tables.push(nextTable);
|
||||||
|
table = nextTable;
|
||||||
|
if (nextLen <= 32) break;
|
||||||
|
}
|
||||||
|
tablesCache[key] = tables;
|
||||||
|
}
|
||||||
|
this.tables = tablesCache[key];
|
||||||
|
this.numFrames = this.tables[0].length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_mirror(x) {
|
||||||
|
return 1 - Math.abs(2 * x - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_toBits(amt, min = 2, max = 12) {
|
||||||
|
const b = max + (min - max) * amt;
|
||||||
|
return { b, n: Math.round(Math.pow(2, b)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
_warpPhase(phase, amt, mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case WarpMode.NONE: {
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
case WarpMode.ASYM: {
|
||||||
|
const a = 0.01 + 0.99 * amt;
|
||||||
|
return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a);
|
||||||
|
}
|
||||||
|
case WarpMode.MIRROR: {
|
||||||
|
// Asym, then mirror
|
||||||
|
return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM));
|
||||||
|
}
|
||||||
|
case WarpMode.BENDP: {
|
||||||
|
return Math.pow(phase, 1 + 3 * amt);
|
||||||
|
}
|
||||||
|
case WarpMode.BENDM: {
|
||||||
|
return Math.pow(phase, 1 / (1 + 3 * amt));
|
||||||
|
}
|
||||||
|
case WarpMode.BENDMP: {
|
||||||
|
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
|
||||||
|
}
|
||||||
|
case WarpMode.SYNC: {
|
||||||
|
const syncRatio = Math.pow(16, amt * amt);
|
||||||
|
return (phase * syncRatio) % 1;
|
||||||
|
}
|
||||||
|
case WarpMode.QUANT: {
|
||||||
|
const { n } = this._toBits(amt);
|
||||||
|
return ffloor(phase * n) / n;
|
||||||
|
}
|
||||||
|
case WarpMode.FOLD: {
|
||||||
|
const K = 7;
|
||||||
|
const k = 1 + Math.max(1, Math.round(K * amt));
|
||||||
|
return Math.abs(frac(k * phase) - 0.5) * 2;
|
||||||
|
}
|
||||||
|
case WarpMode.PWM: {
|
||||||
|
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
|
||||||
|
if (phase < w) return (phase / w) * 0.5;
|
||||||
|
return 0.5 + ((phase - w) / (1 - w)) * 0.5;
|
||||||
|
}
|
||||||
|
case WarpMode.ORBIT: {
|
||||||
|
const depth = 0.5 * amt;
|
||||||
|
const n = 3;
|
||||||
|
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
|
||||||
|
}
|
||||||
|
case WarpMode.SPIN: {
|
||||||
|
const depth = 0.5 * amt;
|
||||||
|
const { n } = this._toBits(amt, 1, 6);
|
||||||
|
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
|
||||||
|
}
|
||||||
|
case WarpMode.CHAOS: {
|
||||||
|
const r = 3.7 + 0.3 * amt;
|
||||||
|
const logistic = r * phase * (1 - phase);
|
||||||
|
return clamp((1 - amt) * phase + amt * logistic, 0, 1);
|
||||||
|
}
|
||||||
|
case WarpMode.PRIMES: {
|
||||||
|
const isPrime = (n) => {
|
||||||
|
if (n < 2) return false;
|
||||||
|
if (n % 2 === 0) return n === 2;
|
||||||
|
for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let { n } = this._toBits(amt, 3);
|
||||||
|
while (!isPrime(n)) n++;
|
||||||
|
return ffloor(phase * n) / n;
|
||||||
|
}
|
||||||
|
case WarpMode.BINARY: {
|
||||||
|
let { b } = this._toBits(amt, 3);
|
||||||
|
b = Math.round(b);
|
||||||
|
const n = 1 << b;
|
||||||
|
const idx = ffloor(phase * n);
|
||||||
|
const ridx = bitReverse(idx, b);
|
||||||
|
return ridx / n;
|
||||||
|
}
|
||||||
|
case WarpMode.MODULAR: {
|
||||||
|
const { n } = this._toBits(amt);
|
||||||
|
const depth = 0.5 * amt;
|
||||||
|
const jump = frac(phase * n) / n;
|
||||||
|
return frac(phase + depth * jump);
|
||||||
|
}
|
||||||
|
case WarpMode.BROWNIAN: {
|
||||||
|
const disp = 0.25 * amt * brownian(64 * phase, 4);
|
||||||
|
return frac(phase + disp);
|
||||||
|
}
|
||||||
|
case WarpMode.RECIPROCAL: {
|
||||||
|
const g = 2 + 4 * amt;
|
||||||
|
const num = phase * g;
|
||||||
|
const den = phase + (1 - phase) * g;
|
||||||
|
const y = den > 1e-12 ? num / den : 0;
|
||||||
|
return clamp(y, 0, 1);
|
||||||
|
}
|
||||||
|
case WarpMode.WORMHOLE: {
|
||||||
|
const gap = clamp(0.8 * amt, 0, 1);
|
||||||
|
const a = 0.5 * (1 - gap);
|
||||||
|
const b = 0.5 * (1 + gap);
|
||||||
|
if (phase < a) return (phase / a) * 0.5;
|
||||||
|
if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b));
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
case WarpMode.LOGISTIC: {
|
||||||
|
let x = phase;
|
||||||
|
const r = 3.6 + 0.4 * amt;
|
||||||
|
const iters = 1 + Math.round(2 * amt);
|
||||||
|
for (let i = 0; i < iters; i++) x = r * x * (1 - x);
|
||||||
|
return clamp(x, 0, 1);
|
||||||
|
}
|
||||||
|
case WarpMode.SIGMOID: {
|
||||||
|
const k = 1 + 10 * amt;
|
||||||
|
const x = phase - 0.5;
|
||||||
|
const y = 1 / (1 + Math.exp(-k * x));
|
||||||
|
const y0 = 1 / (1 + Math.exp(0.5 * k));
|
||||||
|
const y1 = 1 / (1 + Math.exp(-0.5 * k));
|
||||||
|
return (y - y0) / (y1 - y0);
|
||||||
|
}
|
||||||
|
case WarpMode.FRACTAL: {
|
||||||
|
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt;
|
||||||
|
return frac(phase + d);
|
||||||
|
}
|
||||||
|
case WarpMode.FLIP: {
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sampleFrame(frame, phase) {
|
||||||
|
const len = frame.length;
|
||||||
|
const pos = phase * len;
|
||||||
|
let i = pos | 0;
|
||||||
|
if (i >= len) i = 0; // fast wrap
|
||||||
|
const frac = pos - i;
|
||||||
|
const a = frame[i];
|
||||||
|
let i1 = i + 1;
|
||||||
|
if (i1 >= len) i1 = 0;
|
||||||
|
const b = frame[i1];
|
||||||
|
return a + (b - a) * frac;
|
||||||
|
}
|
||||||
|
|
||||||
|
_chooseMip(dphi) {
|
||||||
|
const approxHarm = clamp(dphi, 1e-6, 64);
|
||||||
|
let level = 0;
|
||||||
|
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||||
|
level++;
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(_inputs, outputs, parameters) {
|
||||||
|
if (currentTime >= parameters.end[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentTime <= parameters.begin[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const outL = outputs[0][0];
|
||||||
|
const outR = outputs[0][1] || outputs[0][0];
|
||||||
|
if (!this.tables) {
|
||||||
|
outL.fill(0);
|
||||||
|
if (outR !== outL) outR.set(outL);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < outL.length; i++) {
|
||||||
|
const detune = pv(parameters.detune, i);
|
||||||
|
const freqspread = pv(parameters.freqspread, i);
|
||||||
|
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||||
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
|
const fIdx = idx | 0;
|
||||||
|
const frac = idx - fIdx;
|
||||||
|
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||||
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
|
const voices = pv(parameters.voices, i);
|
||||||
|
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||||
|
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||||
|
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
|
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||||
|
let f = pv(parameters.frequency, i);
|
||||||
|
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||||
|
const normalizer = 1 / Math.sqrt(voices);
|
||||||
|
for (let n = 0; n < voices; n++) {
|
||||||
|
const isOdd = (n & 1) == 1;
|
||||||
|
let gainL = gain1;
|
||||||
|
let gainR = gain2;
|
||||||
|
// invert right and left gain
|
||||||
|
if (isOdd) {
|
||||||
|
gainL = gain2;
|
||||||
|
gainR = gain1;
|
||||||
|
}
|
||||||
|
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
|
||||||
|
const dPhase = fVoice * this.invSR;
|
||||||
|
const level = this._chooseMip(dPhase);
|
||||||
|
const table = this.tables[level];
|
||||||
|
|
||||||
|
// warp phase then sample
|
||||||
|
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
||||||
|
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||||
|
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||||
|
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||||
|
let s = s0 + (s1 - s0) * frac;
|
||||||
|
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||||
|
s = -s;
|
||||||
|
}
|
||||||
|
outL[i] += s * gainL * normalizer;
|
||||||
|
outR[i] += s * gainR * normalizer;
|
||||||
|
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//import { ZZFX } from 'zzfx';
|
//import { ZZFX } from 'zzfx';
|
||||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
import { registerSound } from './superdough.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { buildSamples } from './zzfx_fork.mjs';
|
import { buildSamples } from './zzfx_fork.mjs';
|
||||||
|
|
||||||
export const getZZFX = (value, t) => {
|
export const getZZFX = (value, t) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
||||||
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pattern.wav
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# supradough
|
||||||
|
|
||||||
|
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// this is a poc of how a pattern can be rendered as a wav file using node
|
||||||
|
// run via: node dough-export.mjs
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import WavEncoder from 'wav-encoder';
|
||||||
|
import { evalScope } from '@strudel/core';
|
||||||
|
import { miniAllStrings } from '@strudel/mini';
|
||||||
|
import { Dough } from './dough.mjs';
|
||||||
|
|
||||||
|
await evalScope(
|
||||||
|
import('@strudel/core'),
|
||||||
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/tonal'),
|
||||||
|
// import('@strudel/tonal'),
|
||||||
|
);
|
||||||
|
|
||||||
|
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
|
||||||
|
|
||||||
|
let sampleRate = 48000,
|
||||||
|
cps = 0.4;
|
||||||
|
|
||||||
|
/* await doughsamples('github:eddyflux/crate');
|
||||||
|
await doughsamples('github:eddyflux/wax'); */
|
||||||
|
|
||||||
|
let pat = note('c,eb,g,<bb c4 d4 eb4>')
|
||||||
|
.s('sine')
|
||||||
|
.press()
|
||||||
|
.add(note(24))
|
||||||
|
.fmi(3)
|
||||||
|
.fmh(5.01)
|
||||||
|
.dec(0.4)
|
||||||
|
.delay('.6:<.12 .22>:.8')
|
||||||
|
.jux(press)
|
||||||
|
.rarely(add(note('12')))
|
||||||
|
.lpf(400)
|
||||||
|
.lpq(0.2)
|
||||||
|
.lpd(0.4)
|
||||||
|
.lpenv(3)
|
||||||
|
.fmdecay(0.4)
|
||||||
|
.fmenv(1)
|
||||||
|
.postgain(0.6)
|
||||||
|
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
|
||||||
|
.stack(
|
||||||
|
s('[- white@3]*2')
|
||||||
|
.dec(0.4)
|
||||||
|
.hpf('<2000!3 <4000 8000>>*4')
|
||||||
|
.hpq(0.6)
|
||||||
|
.ply('<1 2>*4')
|
||||||
|
.postgain(0.5)
|
||||||
|
.delay(0.5)
|
||||||
|
.jux(rev)
|
||||||
|
.lpf(5000),
|
||||||
|
)
|
||||||
|
.stack(
|
||||||
|
note('<c2 - [- f1] ->*2')
|
||||||
|
.s('square')
|
||||||
|
.lpf(sine.range(100, 300).slow(4))
|
||||||
|
.lpe(1)
|
||||||
|
.segment(8)
|
||||||
|
.lpd(0.3)
|
||||||
|
.lpq(0.2)
|
||||||
|
.dec(0.2)
|
||||||
|
.speed('<1 2>')
|
||||||
|
.ply('<1 2>')
|
||||||
|
.postgain(1),
|
||||||
|
)
|
||||||
|
.stack(
|
||||||
|
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
|
||||||
|
.voicing()
|
||||||
|
.s('<sine>')
|
||||||
|
.clip(1)
|
||||||
|
.rel(0.4)
|
||||||
|
.vib('4:.2')
|
||||||
|
.gain(0.7)
|
||||||
|
.hpf(1200)
|
||||||
|
.fm(0.5)
|
||||||
|
.att(1)
|
||||||
|
.lpa(0.5)
|
||||||
|
.lpf(200)
|
||||||
|
.lpenv(4)
|
||||||
|
.chorus(0.8),
|
||||||
|
)
|
||||||
|
.slow(1 / cps);
|
||||||
|
|
||||||
|
let cycles = 30;
|
||||||
|
let seconds = cycles + 1; // 1s release tail
|
||||||
|
const haps = pat.queryArc(0, cycles);
|
||||||
|
|
||||||
|
const dough = new Dough(sampleRate);
|
||||||
|
|
||||||
|
console.log('spawn voices...');
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
hap.value._begin = Number(hap.whole.begin);
|
||||||
|
hap.value._duration = hap.duration /* / cps */;
|
||||||
|
dough.scheduleSpawn(hap.value);
|
||||||
|
});
|
||||||
|
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
|
||||||
|
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
|
||||||
|
let t = performance.now();
|
||||||
|
while (dough.t <= buffers[0].length) {
|
||||||
|
dough.update();
|
||||||
|
buffers[0][dough.t] = dough.out[0];
|
||||||
|
buffers[1][dough.t] = dough.out[1];
|
||||||
|
if (dough.t % sampleRate === 0) {
|
||||||
|
process.stdout.write('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const took = (performance.now() - t) / 1000;
|
||||||
|
const load = (took / seconds) * 100;
|
||||||
|
const speed = (seconds / took).toFixed(2);
|
||||||
|
console.log('');
|
||||||
|
console.log(`done!
|
||||||
|
rendered ${seconds}s in ${took.toFixed(2)}s
|
||||||
|
speed: ${speed}x
|
||||||
|
load: ${load.toFixed(2)}%`);
|
||||||
|
|
||||||
|
const patternAudio = {
|
||||||
|
sampleRate,
|
||||||
|
channelData: buffers,
|
||||||
|
};
|
||||||
|
|
||||||
|
WavEncoder.encode(patternAudio).then((buffer) => {
|
||||||
|
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Dough } from './dough.mjs';
|
||||||
|
|
||||||
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
|
|
||||||
|
class DoughProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.dough = new Dough(sampleRate, currentTime);
|
||||||
|
this.port.onmessage = (event) => {
|
||||||
|
if (event.data.spawn) {
|
||||||
|
this.dough.scheduleSpawn(event.data.spawn);
|
||||||
|
} else if (event.data.sample) {
|
||||||
|
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
|
||||||
|
} else if (event.data.samples) {
|
||||||
|
event.data.samples.forEach(([name, channels, sampleRate]) => {
|
||||||
|
this.dough.loadSample(name, channels, sampleRate);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('unrecognized event type', event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
process(inputs, outputs, params) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const output = outputs[0];
|
||||||
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
|
this.dough.update();
|
||||||
|
for (let c = 0; c < output.length; c++) {
|
||||||
|
//prevent speaker blowout via clipping if threshold exceeds
|
||||||
|
output[c][i] = clamp(this.dough.out[c], -1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // keep the audio processing going
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('dough-processor', DoughProcessor);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
|
||||||
|
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
|
||||||
|
|
||||||
|
export * from './dough.mjs';
|
||||||
|
export const workletUrl = _workletUrl;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "supradough",
|
||||||
|
"version": "1.2.4",
|
||||||
|
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.11",
|
||||||
|
"vite-plugin-bundle-audioworklet": "workspace:*",
|
||||||
|
"wav-encoder": "^1.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import './tonal.mjs';
|
import './tonal.mjs';
|
||||||
import './voicings.mjs';
|
import './voicings.mjs';
|
||||||
|
import './ireal.mjs';
|
||||||
|
|
||||||
export * from './tonal.mjs';
|
export * from './tonal.mjs';
|
||||||
export * from './voicings.mjs';
|
export * from './voicings.mjs';
|
||||||
|
export * from './ireal.mjs';
|
||||||
import './ireal.mjs';
|
|
||||||
|
|
||||||
export const packageName = '@strudel/tonal';
|
export const packageName = '@strudel/tonal';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/tonal",
|
"name": "@strudel/tonal",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -7,80 +7,123 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
// import { strict as assert } from 'assert';
|
// import { strict as assert } from 'assert';
|
||||||
|
|
||||||
import '../tonal.mjs'; // need to import this to add prototypes
|
import '../tonal.mjs'; // need to import this to add prototypes
|
||||||
import { pure, n, seq, note } from '@strudel/core';
|
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { mini } from '../../mini/mini.mjs';
|
import { mini } from '../../mini/mini.mjs';
|
||||||
|
|
||||||
describe('tonal', () => {
|
describe('tonal', () => {
|
||||||
it('Should run tonal functions ', () => {
|
describe('scaleTranspose', () => {
|
||||||
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
it('transposes notes by scale degrees', () => {
|
||||||
|
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('scale with plain values', () => {
|
describe('scale', () => {
|
||||||
expect(
|
it('converts plain values', () => {
|
||||||
seq(0, 1, 2)
|
expect(
|
||||||
.scale('C major')
|
seq(0, 1, 2)
|
||||||
.note()
|
.scale('C major')
|
||||||
.firstCycleValues.map((h) => h.note),
|
.note()
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values (mini notation)', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale('C:major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values (no tonic)', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale('major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values (explicit mini notation)', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale(mini('C:major'))
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts decorated n values', () => {
|
||||||
|
expect(
|
||||||
|
n(seq('0b', '1#', '-2', '3##', '4bb'))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
|
||||||
|
});
|
||||||
|
it('snaps notes (upwards) to scale', () => {
|
||||||
|
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
|
||||||
|
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
note(seq(inputNotes))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(expectedNotes);
|
||||||
|
});
|
||||||
|
it('snaps notes to the correct octave', () => {
|
||||||
|
const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
|
||||||
|
const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
note(seq(inputNotes))
|
||||||
|
.scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(expectedNotes);
|
||||||
|
});
|
||||||
|
it('handles scale names provided with colons', () => {
|
||||||
|
const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
|
||||||
|
const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
note(seq(inputNotes))
|
||||||
|
.scale('F#:pentatonic') // F#, G#, A#, C#, and D#
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(expectedNotes);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('scale with n values', () => {
|
describe('transpose', () => {
|
||||||
expect(
|
it('transposes note numbers with interval numbers', () => {
|
||||||
n(seq(0, 1, 2))
|
expect(
|
||||||
.scale('C major')
|
note(seq(40, 40, 40))
|
||||||
.firstCycleValues.map((h) => h.note),
|
.transpose(0, 1, 2)
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
.firstCycleValues.map((h) => h.note),
|
||||||
});
|
).toEqual([40, 41, 42]);
|
||||||
it('scale with colon', () => {
|
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
||||||
expect(
|
});
|
||||||
n(seq(0, 1, 2))
|
it('transposes note numbers with interval strings', () => {
|
||||||
.scale('C:major')
|
expect(
|
||||||
.firstCycleValues.map((h) => h.note),
|
note(seq(40, 40, 40))
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
.transpose('1P', '2M', '3m')
|
||||||
});
|
.firstCycleValues.map((h) => h.note),
|
||||||
it('scale without tonic', () => {
|
).toEqual([40, 42, 43]);
|
||||||
expect(
|
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
||||||
n(seq(0, 1, 2))
|
});
|
||||||
.scale('major')
|
it('transposes note strings with interval numbers', () => {
|
||||||
.firstCycleValues.map((h) => h.note),
|
expect(
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
note(seq('c', 'c', 'c'))
|
||||||
});
|
.transpose(0, 1, 2)
|
||||||
it('scale with mininotation colon', () => {
|
.firstCycleValues.map((h) => h.note),
|
||||||
expect(
|
).toEqual(['C', 'Db', 'D']);
|
||||||
n(seq(0, 1, 2))
|
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
||||||
.scale(mini('C:major'))
|
});
|
||||||
.firstCycleValues.map((h) => h.note),
|
it('transposes note strings with interval strings', () => {
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
expect(
|
||||||
});
|
note(seq('c', 'c', 'c'))
|
||||||
it('transposes note numbers with interval numbers', () => {
|
.transpose('1P', '2M', '3m')
|
||||||
expect(
|
.firstCycleValues.map((h) => h.note),
|
||||||
note(seq(40, 40, 40))
|
).toEqual(['C', 'D', 'Eb']);
|
||||||
.transpose(0, 1, 2)
|
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
||||||
.firstCycleValues.map((h) => h.note),
|
});
|
||||||
).toEqual([40, 41, 42]);
|
|
||||||
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
|
||||||
});
|
|
||||||
it('transposes note numbers with interval strings', () => {
|
|
||||||
expect(
|
|
||||||
note(seq(40, 40, 40))
|
|
||||||
.transpose('1P', '2M', '3m')
|
|
||||||
.firstCycleValues.map((h) => h.note),
|
|
||||||
).toEqual([40, 42, 43]);
|
|
||||||
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
|
||||||
});
|
|
||||||
it('transposes note strings with interval numbers', () => {
|
|
||||||
expect(
|
|
||||||
note(seq('c', 'c', 'c'))
|
|
||||||
.transpose(0, 1, 2)
|
|
||||||
.firstCycleValues.map((h) => h.note),
|
|
||||||
).toEqual(['C', 'Db', 'D']);
|
|
||||||
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
|
||||||
});
|
|
||||||
it('transposes note strings with interval strings', () => {
|
|
||||||
expect(
|
|
||||||
note(seq('c', 'c', 'c'))
|
|
||||||
.transpose('1P', '2M', '3m')
|
|
||||||
.firstCycleValues.map((h) => h.note),
|
|
||||||
).toEqual(['C', 'D', 'Eb']);
|
|
||||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+110
-63
@@ -5,20 +5,28 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||||
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
|
import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core';
|
||||||
import { stepInNamedScale } from './tonleiter.mjs';
|
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||||
|
|
||||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||||
|
|
||||||
function scaleStep(step, scale) {
|
function getScale(scaleName) {
|
||||||
scale = scale.replaceAll(':', ' ');
|
scaleName = scaleName.replaceAll(':', ' ');
|
||||||
step = Math.ceil(step);
|
const scale = Scale.get(scaleName);
|
||||||
let { intervals, tonic, empty } = Scale.get(scale);
|
const { tonic, empty } = scale;
|
||||||
if ((empty && isNote(scale)) || (empty && !tonic)) {
|
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
|
||||||
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
|
throw new Error(
|
||||||
|
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
|
||||||
|
);
|
||||||
} else if (empty) {
|
} else if (empty) {
|
||||||
throw new Error(`invalid scale "${scale}"`);
|
throw new Error(`Invalid scale name "${scaleName}"`);
|
||||||
}
|
}
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaleStep(step, scale) {
|
||||||
|
step = Math.ceil(step);
|
||||||
|
let { intervals, tonic } = getScale(scale);
|
||||||
tonic = tonic || 'C';
|
tonic = tonic || 'C';
|
||||||
const { pc, oct = 3 } = Note.get(tonic);
|
const { pc, oct = 3 } = Note.get(tonic);
|
||||||
const octaveOffset = Math.floor(step / intervals.length);
|
const octaveOffset = Math.floor(step / intervals.length);
|
||||||
@@ -30,8 +38,7 @@ function scaleStep(step, scale) {
|
|||||||
// transpose note inside scale by offset steps
|
// transpose note inside scale by offset steps
|
||||||
// function scaleOffset(scale: string, offset: number, note: string) {
|
// function scaleOffset(scale: string, offset: number, note: string) {
|
||||||
function scaleOffset(scale, offset, note) {
|
function scaleOffset(scale, offset, note) {
|
||||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
let { notes } = getScale(scale);
|
||||||
let { notes } = Scale.get(`${tonic} ${scaleName}`);
|
|
||||||
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
||||||
offset = Number(offset);
|
offset = Number(offset);
|
||||||
if (isNaN(offset)) {
|
if (isNaN(offset)) {
|
||||||
@@ -120,10 +127,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
const interval = !isNaN(Number(intervalOrSemitones))
|
const interval = !isNaN(Number(intervalOrSemitones))
|
||||||
? Interval.fromSemitones(intervalOrSemitones)
|
? Interval.fromSemitones(intervalOrSemitones)
|
||||||
: String(intervalOrSemitones);
|
: String(intervalOrSemitones);
|
||||||
// TODO: move simplify to player to preserve enharmonics
|
const targetNote = Note.transpose(note, interval);
|
||||||
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
|
|
||||||
// TODO: check if this is still relevant..
|
|
||||||
const targetNote = Note.simplify(Note.transpose(note, interval));
|
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
||||||
}
|
}
|
||||||
@@ -171,8 +175,57 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Converts a step value, which is a number optionally decorated with sharps and flats,
|
||||||
|
// to a number and an `offset` number of semitones
|
||||||
|
function _convertStepToNumberAndOffset(step) {
|
||||||
|
let asNumber = Number(step);
|
||||||
|
let offset = 0;
|
||||||
|
if (isNaN(asNumber)) {
|
||||||
|
step = String(step);
|
||||||
|
// Check to see if the step matches the expected format:
|
||||||
|
// - A number (possibly negative)
|
||||||
|
// - Some number of sharps or flats
|
||||||
|
const match = /^(-?\d+)([#bsf]*)$/.exec(step);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
|
||||||
|
}
|
||||||
|
asNumber = Number(match[1]);
|
||||||
|
const accidentals = match[2] || '';
|
||||||
|
offset = getAccidentalsOffset(accidentals);
|
||||||
|
}
|
||||||
|
return [asNumber, offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
let scaleToMidisAndNotes = {};
|
||||||
|
// Finds the nearest scale note to `note`
|
||||||
|
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
||||||
|
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
|
||||||
|
if (scaleToMidisAndNotes[scaleName] === undefined) {
|
||||||
|
const { intervals, tonic } = getScale(scaleName);
|
||||||
|
const { pc } = Note.get(tonic);
|
||||||
|
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
|
||||||
|
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
|
||||||
|
const sMidi = sNotes.map(noteToMidi);
|
||||||
|
// Cache
|
||||||
|
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
|
||||||
|
}
|
||||||
|
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
|
||||||
|
const rootMidi = scaleMidis[0];
|
||||||
|
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
|
||||||
|
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
|
||||||
|
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
|
||||||
|
const noteMatch = scaleNotes[noteIdx];
|
||||||
|
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
|
||||||
|
*
|
||||||
|
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
|
||||||
|
* in the scale as well as sharps or flats to produce notes outside of the scale.
|
||||||
|
*
|
||||||
|
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||||
*
|
*
|
||||||
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||||
*
|
*
|
||||||
@@ -191,8 +244,13 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
|||||||
* n(rand.range(0,12).segment(8))
|
* n(rand.range(0,12).segment(8))
|
||||||
* .scale("C:ritusen")
|
* .scale("C:ritusen")
|
||||||
* .s("piano")
|
* .s("piano")
|
||||||
|
* @example
|
||||||
|
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
|
||||||
|
* .scale("C:<major minor>/2")
|
||||||
|
* .s("piano")
|
||||||
|
* @example
|
||||||
|
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const scale = register(
|
export const scale = register(
|
||||||
'scale',
|
'scale',
|
||||||
function (scale, pat) {
|
function (scale, pat) {
|
||||||
@@ -200,58 +258,47 @@ export const scale = register(
|
|||||||
if (Array.isArray(scale)) {
|
if (Array.isArray(scale)) {
|
||||||
scale = scale.flat().join(' ');
|
scale = scale.flat().join(' ');
|
||||||
}
|
}
|
||||||
return (
|
return pat.withHaps((haps) => {
|
||||||
pat
|
haps = haps.map((hap) => {
|
||||||
.fmap((value) => {
|
let hVal = hap.value;
|
||||||
const isObject = typeof value === 'object';
|
const isObject = typeof hVal === 'object';
|
||||||
let step = isObject ? value.n : value;
|
// If hVal is a pure value, place it on `n` so that we interpret it as a scale degree
|
||||||
if (isObject) {
|
hVal = isObject ? hVal : { n: hVal };
|
||||||
delete value.n; // remove n so it won't cause trouble
|
const { note, n, value, ...otherValues } = hVal;
|
||||||
}
|
const noteOrStep = note ?? n ?? value;
|
||||||
if (isNote(step)) {
|
if (noteOrStep === undefined) {
|
||||||
// legacy..
|
logger(
|
||||||
return pure(step);
|
`[tonal] Invalid value format for 'scale'. Value must contain n, note, or value but received keys [${Object.keys(hVal).join(', ')}]`,
|
||||||
}
|
'error',
|
||||||
let asNumber = Number(step);
|
);
|
||||||
let semitones = 0;
|
return hap; // pass the value through unchanged
|
||||||
if (isNaN(asNumber)) {
|
}
|
||||||
step = String(step);
|
let scaleNote;
|
||||||
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
if (isNote(noteOrStep)) {
|
||||||
logger(
|
// Note case (quantize to scale)
|
||||||
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
scaleNote = _getNearestScaleNote(scale, noteOrStep);
|
||||||
'error',
|
hap.value = { ...otherValues, note: scaleNote };
|
||||||
);
|
} else {
|
||||||
return silence;
|
// Step case (convert to note in scale)
|
||||||
}
|
|
||||||
const isharp = step.indexOf('#');
|
|
||||||
if (isharp >= 0) {
|
|
||||||
asNumber = Number(step.substring(0, isharp));
|
|
||||||
semitones = step.length - isharp;
|
|
||||||
} else {
|
|
||||||
const iflat = step.indexOf('b');
|
|
||||||
asNumber = Number(step.substring(0, iflat));
|
|
||||||
semitones = iflat - step.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
let note;
|
const [number, offset] = _convertStepToNumberAndOffset(noteOrStep);
|
||||||
if (isObject && value.anchor) {
|
if (otherValues.anchor) {
|
||||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
scaleNote = stepInNamedScale(number, scale, otherValues.anchor);
|
||||||
} else {
|
} else {
|
||||||
note = scaleStep(asNumber, scale);
|
scaleNote = scaleStep(number, scale);
|
||||||
}
|
}
|
||||||
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
|
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
|
||||||
value = pure(isObject ? { ...value, note } : note);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[tonal] ${err.message}`, 'error');
|
logger(`[tonal] ${err.message}`, 'error');
|
||||||
value = silence;
|
return; // will be removed
|
||||||
}
|
}
|
||||||
return value;
|
}
|
||||||
})
|
hap.value = isObject ? { ...otherValues, note: scaleNote } : scaleNote;
|
||||||
.outerJoin()
|
// Tag with scale for downsteam scale-aware operations
|
||||||
// legacy:
|
return hap.setContext({ ...hap.context, scale });
|
||||||
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
|
});
|
||||||
);
|
return removeUndefineds(haps);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
true, // preserve step count
|
true, // preserve step count
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
|
|||||||
let scaleSteps = {}; // [scaleName]: semitones[]
|
let scaleSteps = {}; // [scaleName]: semitones[]
|
||||||
|
|
||||||
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||||
let [root, scaleName] = Scale.tokenize(scale);
|
const [root, scaleName] = Scale.tokenize(scale);
|
||||||
const rootMidi = x2midi(root);
|
const rootMidi = x2midi(root);
|
||||||
const rootChroma = midi2chroma(rootMidi);
|
const rootChroma = midi2chroma(rootMidi);
|
||||||
if (!scaleSteps[scaleName]) {
|
if (!scaleSteps[scaleName]) {
|
||||||
let { intervals } = Scale.get(`C ${scaleName}`);
|
const { intervals } = Scale.get(`C ${scaleName}`);
|
||||||
// cache result
|
// cache result
|
||||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||||
}
|
}
|
||||||
@@ -222,6 +222,7 @@ export const Note = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO: support octave numbers
|
// TODO: support octave numbers
|
||||||
|
// Example: Note("Bb3").transpose("c3")
|
||||||
export function transpose(note, step) {
|
export function transpose(note, step) {
|
||||||
// example: E, 3
|
// example: E, 3
|
||||||
const stepNumber = Step.tokenize(step)[1]; // 3
|
const stepNumber = Step.tokenize(step)[1]; // 3
|
||||||
@@ -235,5 +236,3 @@ export function transpose(note, step) {
|
|||||||
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
|
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
|
||||||
return [targetNote, offsetAccidentals].join('');
|
return [targetNote, offsetAccidentals].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Note("Bb3").transpose("c3")
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/transpiler",
|
"name": "@strudel/transpiler",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/web",
|
"name": "@strudel/web",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||||
"module": "web.mjs",
|
"module": "web.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
export * from './webaudio.mjs';
|
export * from './webaudio.mjs';
|
||||||
export * from './scope.mjs';
|
export * from './scope.mjs';
|
||||||
export * from './spectrum.mjs';
|
export * from './spectrum.mjs';
|
||||||
|
export * from './supradough.mjs';
|
||||||
export * from 'superdough';
|
export * from 'superdough';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/webaudio",
|
"name": "@strudel/webaudio",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"description": "Web Audio helpers for Strudel",
|
"description": "Web Audio helpers for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"superdough": "workspace:*"
|
"superdough": "workspace:*",
|
||||||
|
"supradough": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Pattern } from '@strudel/core';
|
||||||
|
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
|
||||||
|
|
||||||
|
let doughWorklet;
|
||||||
|
|
||||||
|
function initDoughWorklet() {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
doughWorklet = getWorklet(
|
||||||
|
ac,
|
||||||
|
'dough-processor',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
outputChannelCount: [2],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
connectToDestination(doughWorklet); // channels?
|
||||||
|
}
|
||||||
|
|
||||||
|
const soundMap = new Map();
|
||||||
|
const loadedSounds = new Map();
|
||||||
|
|
||||||
|
Pattern.prototype.supradough = function () {
|
||||||
|
return this.onTrigger((hap, __, cps, begin) => {
|
||||||
|
hap.value._begin = begin;
|
||||||
|
hap.value._duration = hap.duration / cps;
|
||||||
|
!doughWorklet && initDoughWorklet();
|
||||||
|
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
|
||||||
|
const n = hap.value.n ?? 0;
|
||||||
|
const soundKey = `${s}:${n}`;
|
||||||
|
if (soundMap.has(s)) {
|
||||||
|
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
|
||||||
|
}
|
||||||
|
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
|
||||||
|
const urls = soundMap.get(s);
|
||||||
|
const url = urls[n % urls.length];
|
||||||
|
console.log(`load ${soundKey} from ${url}`);
|
||||||
|
const loadSample = fetchSample(url);
|
||||||
|
loadedSounds.set(soundKey, loadSample);
|
||||||
|
loadSample.then(({ channels, sampleRate }) =>
|
||||||
|
doughWorklet.port.postMessage({
|
||||||
|
sample: soundKey,
|
||||||
|
channels,
|
||||||
|
sampleRate,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
doughWorklet.port.postMessage({ spawn: hap.value });
|
||||||
|
}, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
function githubPath(base, subpath = '') {
|
||||||
|
if (!base.startsWith('github:')) {
|
||||||
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
|
}
|
||||||
|
let [_, path] = base.split('github:');
|
||||||
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
if (path.split('/').length === 2) {
|
||||||
|
// assume main as default branch if none set
|
||||||
|
path += '/main';
|
||||||
|
}
|
||||||
|
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||||
|
}
|
||||||
|
export async function fetchSampleMap(url) {
|
||||||
|
if (url.startsWith('github:')) {
|
||||||
|
url = githubPath(url, 'strudel.json');
|
||||||
|
}
|
||||||
|
if (url.startsWith('local:')) {
|
||||||
|
url = `http://localhost:5432`;
|
||||||
|
}
|
||||||
|
if (url.startsWith('shabda:')) {
|
||||||
|
let [_, path] = url.split('shabda:');
|
||||||
|
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||||
|
}
|
||||||
|
if (url.startsWith('shabda/speech')) {
|
||||||
|
let [_, path] = url.split('shabda/speech');
|
||||||
|
path = path.startsWith('/') ? path.substring(1) : path;
|
||||||
|
let [params, words] = path.split(':');
|
||||||
|
let gender = 'f';
|
||||||
|
let language = 'en-GB';
|
||||||
|
if (params) {
|
||||||
|
[language, gender] = params.split('/');
|
||||||
|
}
|
||||||
|
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||||
|
}
|
||||||
|
if (typeof fetch !== 'function') {
|
||||||
|
// not a browser
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base = url.split('/').slice(0, -1).join('/');
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
// skip fetch when in node / testing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = await fetch(url)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`error loading "${url}"`);
|
||||||
|
});
|
||||||
|
return [json, json._base || base];
|
||||||
|
}
|
||||||
|
|
||||||
|
// for some reason, only piano and flute work.. is it because mp3??
|
||||||
|
|
||||||
|
async function fetchSample(url) {
|
||||||
|
const buffer = await fetch(url)
|
||||||
|
.then((res) => res.arrayBuffer())
|
||||||
|
.then((buf) => getAudioContext().decodeAudioData(buf));
|
||||||
|
let channels = [];
|
||||||
|
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
||||||
|
channels.push(buffer.getChannelData(i));
|
||||||
|
}
|
||||||
|
return { channels, sampleRate: buffer.sampleRate };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function doughsamples(sampleMap, baseUrl) {
|
||||||
|
if (typeof sampleMap === 'string') {
|
||||||
|
const [json, base] = await fetchSampleMap(sampleMap);
|
||||||
|
// console.log('json', json, 'base', base);
|
||||||
|
return doughsamples(json, base);
|
||||||
|
}
|
||||||
|
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||||
|
if (key !== '_base') {
|
||||||
|
urls = urls.map((url) => baseUrl + url);
|
||||||
|
// console.log('set', key, urls);
|
||||||
|
soundMap.set(key, urls);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as strudel from '@strudel/core';
|
import * as strudel from '@strudel/core';
|
||||||
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
|
||||||
|
import './supradough.mjs';
|
||||||
|
import { workletUrl } from 'supradough';
|
||||||
|
|
||||||
|
registerWorklet(workletUrl);
|
||||||
|
|
||||||
const { Pattern, logger, repl } = strudel;
|
const { Pattern, logger, repl } = strudel;
|
||||||
|
|
||||||
setLogger(logger);
|
setLogger(logger);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/xen",
|
"name": "@strudel/xen",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "Xenharmonic API for strudel",
|
"description": "Xenharmonic API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
|||||||
|
|
||||||
/* Load a new scale */
|
/* Load a new scale */
|
||||||
|
|
||||||
Tune.prototype.loadScale = function(name){
|
Tune.prototype.loadScale = function(scale){
|
||||||
|
|
||||||
/* load the scale */
|
/* load the scale */
|
||||||
var freqs = TuningList[name].frequencies
|
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||||
this.scale = []
|
this.scale = []
|
||||||
for (var i=0;i<freqs.length-1;i++) {
|
for (var i=0;i<freqs.length-1;i++) {
|
||||||
this.scale.push(freqs[i]/freqs[0])
|
this.scale.push(freqs[i]/freqs[0])
|
||||||
@@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
|
|||||||
return possible
|
return possible
|
||||||
}
|
}
|
||||||
|
|
||||||
Tune.prototype.isValidScale = function(name) {
|
function isArrayOfNumbers(arg) {
|
||||||
return !!TuningList[name];
|
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* allow an array of values too */
|
||||||
|
Tune.prototype.isValidScale = function(scale) {
|
||||||
|
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return a collection of notes as an array */
|
/* Return a collection of notes as an array */
|
||||||
|
|||||||
Generated
+279
-18
@@ -41,7 +41,7 @@ importers:
|
|||||||
version: 2.2.7
|
version: 2.2.7
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: 3.0.4
|
specifier: 3.0.4
|
||||||
version: 3.0.4(vitest@3.0.4)
|
version: 3.0.4(vitest@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))
|
||||||
'@vitest/ui':
|
'@vitest/ui':
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(vitest@3.0.4)
|
version: 3.0.4(vitest@3.0.4)
|
||||||
@@ -201,8 +201,8 @@ importers:
|
|||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
'@replit/codemirror-vim':
|
'@replit/codemirror-vim':
|
||||||
specifier: ^6.2.1
|
specifier: ^6.3.0
|
||||||
version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
'@replit/codemirror-vscode-keymap':
|
'@replit/codemirror-vscode-keymap':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
@@ -212,12 +212,21 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../draw
|
version: link:../draw
|
||||||
|
'@strudel/tonal':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../tonal
|
||||||
'@strudel/transpiler':
|
'@strudel/transpiler':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../transpiler
|
version: link:../transpiler
|
||||||
|
'@tonaljs/tonal':
|
||||||
|
specifier: ^4.10.0
|
||||||
|
version: 4.10.0
|
||||||
nanostores:
|
nanostores:
|
||||||
specifier: ^0.11.3
|
specifier: ^0.11.3
|
||||||
version: 0.11.3
|
version: 0.11.3
|
||||||
|
superdough:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../superdough
|
||||||
devDependencies:
|
devDependencies:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
@@ -406,9 +415,12 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
osc-js:
|
osc:
|
||||||
specifier: ^2.4.1
|
specifier: ^2.4.5
|
||||||
version: 2.4.1
|
version: 2.4.5
|
||||||
|
ws:
|
||||||
|
specifier: ^8.18.3
|
||||||
|
version: 8.18.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
pkg:
|
pkg:
|
||||||
specifier: ^5.8.1
|
specifier: ^5.8.1
|
||||||
@@ -517,6 +529,18 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../vite-plugin-bundle-audioworklet
|
version: link:../vite-plugin-bundle-audioworklet
|
||||||
|
|
||||||
|
packages/supradough:
|
||||||
|
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)
|
||||||
|
vite-plugin-bundle-audioworklet:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../vite-plugin-bundle-audioworklet
|
||||||
|
wav-encoder:
|
||||||
|
specifier: ^1.3.0
|
||||||
|
version: 1.3.0
|
||||||
|
|
||||||
packages/tidal:
|
packages/tidal:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
@@ -625,6 +649,9 @@ importers:
|
|||||||
superdough:
|
superdough:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../superdough
|
version: link:../superdough
|
||||||
|
supradough:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../supradough
|
||||||
devDependencies:
|
devDependencies:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
@@ -2232,14 +2259,14 @@ packages:
|
|||||||
'@codemirror/state': ^6.0.1
|
'@codemirror/state': ^6.0.1
|
||||||
'@codemirror/view': ^6.3.0
|
'@codemirror/view': ^6.3.0
|
||||||
|
|
||||||
'@replit/codemirror-vim@6.2.1':
|
'@replit/codemirror-vim@6.3.0':
|
||||||
resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==}
|
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@codemirror/commands': ^6.0.0
|
'@codemirror/commands': 6.x.x
|
||||||
'@codemirror/language': ^6.1.0
|
'@codemirror/language': 6.x.x
|
||||||
'@codemirror/search': ^6.2.0
|
'@codemirror/search': 6.x.x
|
||||||
'@codemirror/state': ^6.0.1
|
'@codemirror/state': 6.x.x
|
||||||
'@codemirror/view': ^6.0.3
|
'@codemirror/view': 6.x.x
|
||||||
|
|
||||||
'@replit/codemirror-vscode-keymap@6.0.2':
|
'@replit/codemirror-vscode-keymap@6.0.2':
|
||||||
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
|
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
|
||||||
@@ -2405,6 +2432,70 @@ packages:
|
|||||||
'@rtsao/scc@1.1.0':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
|
'@serialport/binding-mock@10.2.2':
|
||||||
|
resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/bindings-cpp@12.0.1':
|
||||||
|
resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@serialport/bindings-interface@1.2.2':
|
||||||
|
resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
|
||||||
|
engines: {node: ^12.22 || ^14.13 || >=16}
|
||||||
|
|
||||||
|
'@serialport/parser-byte-length@12.0.0':
|
||||||
|
resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-cctalk@12.0.0':
|
||||||
|
resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-delimiter@11.0.0':
|
||||||
|
resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-delimiter@12.0.0':
|
||||||
|
resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-inter-byte-timeout@12.0.0':
|
||||||
|
resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-packet-length@12.0.0':
|
||||||
|
resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
|
||||||
|
engines: {node: '>=8.6.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-readline@11.0.0':
|
||||||
|
resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-readline@12.0.0':
|
||||||
|
resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-ready@12.0.0':
|
||||||
|
resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-regex@12.0.0':
|
||||||
|
resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-slip-encoder@12.0.0':
|
||||||
|
resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/parser-spacepacket@12.0.0':
|
||||||
|
resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
'@serialport/stream@12.0.0':
|
||||||
|
resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
'@shikijs/core@1.29.1':
|
'@shikijs/core@1.29.1':
|
||||||
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
|
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
|
||||||
|
|
||||||
@@ -3657,6 +3748,15 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
debug@4.3.4:
|
||||||
|
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
debug@4.4.0:
|
debug@4.4.0:
|
||||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -5266,6 +5366,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
long@4.0.0:
|
||||||
|
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
||||||
|
|
||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
@@ -5649,6 +5752,9 @@ packages:
|
|||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
|
ms@2.1.2:
|
||||||
|
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -5707,6 +5813,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==}
|
resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
node-addon-api@7.0.0:
|
||||||
|
resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
|
||||||
|
|
||||||
node-addon-api@8.3.0:
|
node-addon-api@8.3.0:
|
||||||
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
|
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
|
||||||
engines: {node: ^18 || ^20 || >= 21}
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
@@ -5714,6 +5823,7 @@ packages:
|
|||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
|
deprecated: Use your platform's native DOMException instead
|
||||||
|
|
||||||
node-fetch-native@1.6.6:
|
node-fetch-native@1.6.6:
|
||||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
||||||
@@ -5744,6 +5854,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==}
|
resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
|
node-gyp-build@4.6.0:
|
||||||
|
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-gyp-build@4.8.4:
|
node-gyp-build@4.8.4:
|
||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -5902,8 +6016,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
osc-js@2.4.1:
|
osc@2.4.5:
|
||||||
resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==}
|
resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||||
@@ -6664,6 +6778,10 @@ packages:
|
|||||||
serialize-javascript@4.0.0:
|
serialize-javascript@4.0.0:
|
||||||
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
|
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
|
||||||
|
|
||||||
|
serialport@12.0.0:
|
||||||
|
resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
set-blocking@2.0.0:
|
set-blocking@2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
@@ -6751,6 +6869,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
slip@1.0.2:
|
||||||
|
resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==}
|
||||||
|
|
||||||
smart-buffer@4.2.0:
|
smart-buffer@4.2.0:
|
||||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||||
@@ -6803,6 +6924,7 @@ packages:
|
|||||||
source-map@0.8.0-beta.0:
|
source-map@0.8.0-beta.0:
|
||||||
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
deprecated: The work that was done in this beta branch won't be included in future versions
|
||||||
|
|
||||||
sourcemap-codec@1.4.8:
|
sourcemap-codec@1.4.8:
|
||||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||||
@@ -7535,6 +7657,9 @@ packages:
|
|||||||
walk-up-path@3.0.1:
|
walk-up-path@3.0.1:
|
||||||
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
||||||
|
|
||||||
|
wav-encoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
|
||||||
|
|
||||||
wav@1.0.2:
|
wav@1.0.2:
|
||||||
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
||||||
|
|
||||||
@@ -7622,6 +7747,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
|
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
wolfy87-eventemitter@5.2.9:
|
||||||
|
resolution: {integrity: sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==}
|
||||||
|
|
||||||
word-wrap@1.2.5:
|
word-wrap@1.2.5:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -7657,6 +7785,7 @@ packages:
|
|||||||
|
|
||||||
workbox-google-analytics@7.0.0:
|
workbox-google-analytics@7.0.0:
|
||||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||||
|
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||||
|
|
||||||
workbox-navigation-preload@7.0.0:
|
workbox-navigation-preload@7.0.0:
|
||||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||||
@@ -7743,6 +7872,18 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ws@8.18.3:
|
||||||
|
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
xmlcreate@2.0.4:
|
xmlcreate@2.0.4:
|
||||||
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
|
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
|
||||||
|
|
||||||
@@ -9595,7 +9736,7 @@ snapshots:
|
|||||||
'@codemirror/state': 6.5.1
|
'@codemirror/state': 6.5.1
|
||||||
'@codemirror/view': 6.36.2
|
'@codemirror/view': 6.36.2
|
||||||
|
|
||||||
'@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/commands': 6.8.0
|
'@codemirror/commands': 6.8.0
|
||||||
'@codemirror/language': 6.10.8
|
'@codemirror/language': 6.10.8
|
||||||
@@ -9739,6 +9880,76 @@ snapshots:
|
|||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
|
'@serialport/binding-mock@10.2.2':
|
||||||
|
dependencies:
|
||||||
|
'@serialport/bindings-interface': 1.2.2
|
||||||
|
debug: 4.4.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/bindings-cpp@12.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@serialport/bindings-interface': 1.2.2
|
||||||
|
'@serialport/parser-readline': 11.0.0
|
||||||
|
debug: 4.3.4
|
||||||
|
node-addon-api: 7.0.0
|
||||||
|
node-gyp-build: 4.6.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/bindings-interface@1.2.2':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-byte-length@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-cctalk@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-delimiter@11.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-delimiter@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-inter-byte-timeout@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-packet-length@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-readline@11.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@serialport/parser-delimiter': 11.0.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-readline@12.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@serialport/parser-delimiter': 12.0.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-ready@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-regex@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-slip-encoder@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/parser-spacepacket@12.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@serialport/stream@12.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@serialport/bindings-interface': 1.2.2
|
||||||
|
debug: 4.3.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@shikijs/core@1.29.1':
|
'@shikijs/core@1.29.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/engine-javascript': 1.29.1
|
'@shikijs/engine-javascript': 1.29.1
|
||||||
@@ -10343,7 +10554,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
|
'@vitest/coverage-v8@3.0.4(vitest@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))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
@@ -11251,6 +11462,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
debug@4.3.4:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
debug@4.4.0:
|
debug@4.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -13120,6 +13336,8 @@ snapshots:
|
|||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
is-unicode-supported: 0.1.0
|
is-unicode-supported: 0.1.0
|
||||||
|
|
||||||
|
long@4.0.0: {}
|
||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
loupe@3.1.2: {}
|
loupe@3.1.2: {}
|
||||||
@@ -13797,6 +14015,9 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.0.0: {}
|
ms@2.0.0: {}
|
||||||
|
|
||||||
|
ms@2.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
multimatch@5.0.0:
|
multimatch@5.0.0:
|
||||||
@@ -13846,6 +14067,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.6.3
|
semver: 7.6.3
|
||||||
|
|
||||||
|
node-addon-api@7.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-addon-api@8.3.0: {}
|
node-addon-api@8.3.0: {}
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
@@ -13872,6 +14096,9 @@ snapshots:
|
|||||||
|
|
||||||
node-getopt@0.3.2: {}
|
node-getopt@0.3.2: {}
|
||||||
|
|
||||||
|
node-gyp-build@4.6.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-gyp-build@4.8.4: {}
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-gyp@10.3.1:
|
node-gyp@10.3.1:
|
||||||
@@ -14123,11 +14350,17 @@ snapshots:
|
|||||||
|
|
||||||
os-tmpdir@1.0.2: {}
|
os-tmpdir@1.0.2: {}
|
||||||
|
|
||||||
osc-js@2.4.1:
|
osc@2.4.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
long: 4.0.0
|
||||||
|
slip: 1.0.2
|
||||||
|
wolfy87-eventemitter: 5.2.9
|
||||||
ws: 8.18.0
|
ws: 8.18.0
|
||||||
|
optionalDependencies:
|
||||||
|
serialport: 12.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
@@ -15022,6 +15255,26 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
|
|
||||||
|
serialport@12.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@serialport/binding-mock': 10.2.2
|
||||||
|
'@serialport/bindings-cpp': 12.0.1
|
||||||
|
'@serialport/parser-byte-length': 12.0.0
|
||||||
|
'@serialport/parser-cctalk': 12.0.0
|
||||||
|
'@serialport/parser-delimiter': 12.0.0
|
||||||
|
'@serialport/parser-inter-byte-timeout': 12.0.0
|
||||||
|
'@serialport/parser-packet-length': 12.0.0
|
||||||
|
'@serialport/parser-readline': 12.0.0
|
||||||
|
'@serialport/parser-ready': 12.0.0
|
||||||
|
'@serialport/parser-regex': 12.0.0
|
||||||
|
'@serialport/parser-slip-encoder': 12.0.0
|
||||||
|
'@serialport/parser-spacepacket': 12.0.0
|
||||||
|
'@serialport/stream': 12.0.0
|
||||||
|
debug: 4.3.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
set-blocking@2.0.0: {}
|
set-blocking@2.0.0: {}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
@@ -15164,6 +15417,8 @@ snapshots:
|
|||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
|
|
||||||
|
slip@1.0.2: {}
|
||||||
|
|
||||||
smart-buffer@4.2.0: {}
|
smart-buffer@4.2.0: {}
|
||||||
|
|
||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
@@ -15955,6 +16210,8 @@ snapshots:
|
|||||||
|
|
||||||
walk-up-path@3.0.1: {}
|
walk-up-path@3.0.1: {}
|
||||||
|
|
||||||
|
wav-encoder@1.3.0: {}
|
||||||
|
|
||||||
wav@1.0.2:
|
wav@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-alloc: 1.2.0
|
buffer-alloc: 1.2.0
|
||||||
@@ -16071,6 +16328,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width: 7.2.0
|
string-width: 7.2.0
|
||||||
|
|
||||||
|
wolfy87-eventemitter@5.2.9: {}
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
wordwrap@0.0.3: {}
|
wordwrap@0.0.3: {}
|
||||||
@@ -16270,6 +16529,8 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.18.0: {}
|
ws@8.18.0: {}
|
||||||
|
|
||||||
|
ws@8.18.3: {}
|
||||||
|
|
||||||
xmlcreate@2.0.4: {}
|
xmlcreate@2.0.4: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
xtend@4.0.2: {}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,14 @@ const skippedExamples = [
|
|||||||
'accelerationZ',
|
'accelerationZ',
|
||||||
'accelerationY',
|
'accelerationY',
|
||||||
'accelerationX',
|
'accelerationX',
|
||||||
|
'vibrate',
|
||||||
|
'vibGamepadIndex',
|
||||||
|
'vibGamepadId',
|
||||||
|
'vibEnable',
|
||||||
|
'vibWeak',
|
||||||
|
'vibStrong',
|
||||||
|
'vibDuration',
|
||||||
|
'vibIntensity',
|
||||||
'defaultmidimap',
|
'defaultmidimap',
|
||||||
'midimaps',
|
'midimaps',
|
||||||
];
|
];
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,7 @@ import '@strudel/xen/xen.mjs';
|
|||||||
import '../website/src/repl/piano';
|
import '../website/src/repl/piano';
|
||||||
//import * as motionHelpers from '../packages/motion/index.mjs';
|
//import * as motionHelpers from '../packages/motion/index.mjs';
|
||||||
//import * as geolocationHelpers from '../packages/geolocation/index.mjs';
|
//import * as geolocationHelpers from '../packages/geolocation/index.mjs';
|
||||||
import * as gamepadHelpers from '../packages/gamepad/index.mjs';
|
//import * as gamepadHelpers from '../packages/gamepad/index.mjs';
|
||||||
|
|
||||||
class MockedNode {
|
class MockedNode {
|
||||||
chain() {
|
chain() {
|
||||||
@@ -141,8 +141,8 @@ evalScope(
|
|||||||
uiHelpersMocked,
|
uiHelpersMocked,
|
||||||
webaudio,
|
webaudio,
|
||||||
tonalHelpers,
|
tonalHelpers,
|
||||||
gamepadHelpers,
|
|
||||||
/*
|
/*
|
||||||
|
gamepadHelpers,
|
||||||
toneHelpers,
|
toneHelpers,
|
||||||
voicingHelpers,
|
voicingHelpers,
|
||||||
drawHelpers,
|
drawHelpers,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
The FontStruction “TIC-80 wide font”
|
||||||
|
(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed
|
||||||
|
under a Creative Commons CC0 Public Domain Dedication license
|
||||||
|
(http://creativecommons.org/publicdomain/zero/1.0/).
|
||||||
|
[ancestry]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
The font file in this archive was created using Fontstruct the free, online
|
||||||
|
font-building tool.
|
||||||
|
This font was created by “nesbox”.
|
||||||
|
This font has a homepage where this archive and other versions may be found:
|
||||||
|
https://fontstruct.com/fontstructions/show/1388526
|
||||||
|
[ancestry]
|
||||||
|
Try Fontstruct at https://fontstruct.com
|
||||||
|
It’s easy and it’s fun.
|
||||||
|
|
||||||
|
Fontstruct is copyright ©2017-2025 Rob Meek
|
||||||
|
|
||||||
|
LEGAL NOTICE:
|
||||||
|
In using this font you must comply with the licensing terms described in the
|
||||||
|
file “license.txt” included with this archive.
|
||||||
|
If you redistribute the font file in this archive, it must be accompanied by all
|
||||||
|
the other files from this archive, including this one.
|
||||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/",
|
||||||
|
"wt_digital": [
|
||||||
|
"wt_digital/wt_bad_day.wav",
|
||||||
|
"wt_digital/wt_basique.wav",
|
||||||
|
"wt_digital/wt_crickets.wav",
|
||||||
|
"wt_digital/wt_curses.wav",
|
||||||
|
"wt_digital/wt_echoes.wav"
|
||||||
|
],
|
||||||
|
"wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"],
|
||||||
|
"wt_digital_basique": ["wt_digital/wt_basique.wav"],
|
||||||
|
"wt_digital_crickets": ["wt_digital/wt_crickets.wav"],
|
||||||
|
"wt_digital_curses": ["wt_digital/wt_curses.wav"],
|
||||||
|
"wt_digital_echoes": ["wt_digital/wt_echoes.wav"],
|
||||||
|
"wt_vgame": [
|
||||||
|
"wt_vgame/wt_vgame10.wav",
|
||||||
|
"wt_vgame/wt_vgame11.wav",
|
||||||
|
"wt_vgame/wt_vgame12.wav",
|
||||||
|
"wt_vgame/wt_vgame13.wav",
|
||||||
|
"wt_vgame/wt_vgame14.wav",
|
||||||
|
"wt_vgame/wt_vgame15.wav",
|
||||||
|
"wt_vgame/wt_vgame16.wav",
|
||||||
|
"wt_vgame/wt_vgame17.wav",
|
||||||
|
"wt_vgame/wt_vgame18.wav",
|
||||||
|
"wt_vgame/wt_vgame19.wav",
|
||||||
|
"wt_vgame/wt_vgame20.wav"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
|
|||||||
Understand: [
|
Understand: [
|
||||||
{ text: 'Coding syntax', link: 'learn/code' },
|
{ text: 'Coding syntax', link: 'learn/code' },
|
||||||
{ text: 'Pitch', link: 'understand/pitch' },
|
{ text: 'Pitch', link: 'understand/pitch' },
|
||||||
|
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
||||||
{ text: 'Cycles', link: 'understand/cycles' },
|
{ text: 'Cycles', link: 'understand/cycles' },
|
||||||
{ text: 'Voicings', link: 'understand/voicings' },
|
{ text: 'Voicings', link: 'understand/voicings' },
|
||||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
|
|||||||
|
|
||||||
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
||||||
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
||||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
|
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
|
||||||
|
|
||||||
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
||||||
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
||||||
@@ -60,6 +60,23 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
|
|||||||
.room(0.5)`}
|
.room(0.5)`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
## Write your own chained function
|
||||||
|
|
||||||
|
You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`const effectChain = register('effectChain', (pat) => pat
|
||||||
|
.s("sawtooth")
|
||||||
|
.cutoff(500)
|
||||||
|
//.delay(0.5)
|
||||||
|
.room(0.5)
|
||||||
|
)
|
||||||
|
note("a3 c#4 e4 a4").effectChain()`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Try adding `.rev()` after `effectChain()` to hear further effects added.
|
||||||
|
|
||||||
# Comments
|
# Comments
|
||||||
|
|
||||||
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co
|
|||||||
|
|
||||||
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
|
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
|
||||||
|
|
||||||
|
You can also create custom aliases for existing sounds using the `soundAlias` function:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`soundAlias("RolandTR808_bd", "kick")
|
||||||
|
s("kick")`}
|
||||||
|
/>
|
||||||
|
|
||||||
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
|
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
|
||||||
This behaviour of loading things only when they are needed is also called `lazy loading`.
|
This behaviour of loading things only when they are needed is also called `lazy loading`.
|
||||||
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
|
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
|
||||||
@@ -373,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
|||||||
|
|
||||||
### scrub
|
### scrub
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
|
<JsDoc client:idle name="Pattern.scrub" h={0} />
|
||||||
|
|
||||||
### speed
|
### speed
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user