diff --git a/.github/workflows/tauri.yml b/.github/workflows/tauri.yml deleted file mode 100644 index 07fd91be8..000000000 --- a/.github/workflows/tauri.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Tauri Builder -on: - push: - tags: - - "v*" - workflow_dispatch: - -jobs: - release: - strategy: - fail-fast: false - matrix: - platform: [macos-latest, ubuntu-latest, windows-latest] - include: - - os: ubuntu-latest - rust_target: x86_64-unknown-linux-gnu - - os: macos-latest - rust_target: x86_64-apple-darwin - - os: macos-latest - rust_target: aarch64-apple-darwin - - os: windows-latest - rust_target: x86_64-pc-windows-msvc - runs-on: ${{ matrix.platform }} - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - with: - version: 8.6.2 - - - name: Node.js setup - uses: actions/setup-node@v3 - with: - node-version: latest - # node-version-file: '.nvmrc' - - - name: Install Rust (Stable) - run: - curl https://sh.rustup.rs -sSf | sh -s -- -y - - - name: Install dependencies (ubuntu only) - if: matrix.platform == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev - - - name: Install app dependencies from lockfile and build web - run: pnpm install - - - name: Build the app - uses: tauri-apps/tauri-action@v0 - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - # tauri-action replaces \_\_VERSION\_\_ with the app version - tagName: ${{ github.ref_name }} - releaseName: "Strudel v__VERSION__" - releaseBody: | - See the assets to download this version and install. - releaseDraft: true - prerelease: false diff --git a/package.json b/package.json index df50f1f41..01c3e7d5d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "jsdoc-json": "^2.0.2", "lerna": "^8.1.9", "prettier": "^3.4.2", - "vitest": "^3.0.4" + "vitest": "^3.0.4", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 44326b7c7..5036cd8a3 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.1.0", + "version": "1.2.2", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/codemirror/themes/CutiePi.mjs b/packages/codemirror/themes/CutiePi.mjs index f649a1174..64b38eaa7 100644 --- a/packages/codemirror/themes/CutiePi.mjs +++ b/packages/codemirror/themes/CutiePi.mjs @@ -1,5 +1,5 @@ -/** - * @name Cutie Pi +/* + * Cutie Pi * by Switch Angel */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/androidstudio.mjs b/packages/codemirror/themes/androidstudio.mjs index 097f7951b..77c16c331 100644 --- a/packages/codemirror/themes/androidstudio.mjs +++ b/packages/codemirror/themes/androidstudio.mjs @@ -1,5 +1,5 @@ -/** - * @name androidstudio +/* + * androidstudio */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index 4c9fdec6e..ab9cd3607 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -1,5 +1,5 @@ -/** - * @name Atom One +/* + * Atom One * Atom One dark syntax theme * * https://github.com/atom/one-dark-syntax diff --git a/packages/codemirror/themes/darcula.mjs b/packages/codemirror/themes/darcula.mjs index 176acbfe3..ead66e663 100644 --- a/packages/codemirror/themes/darcula.mjs +++ b/packages/codemirror/themes/darcula.mjs @@ -1,6 +1,5 @@ -/** - * @name darcula - * @author darcula +/* + * darcula * Name: IntelliJ IDEA darcula theme * From IntelliJ IDEA by JetBrains */ diff --git a/packages/codemirror/themes/dracula.mjs b/packages/codemirror/themes/dracula.mjs index 10147edaa..bdd9617de 100644 --- a/packages/codemirror/themes/dracula.mjs +++ b/packages/codemirror/themes/dracula.mjs @@ -1,6 +1,5 @@ -/** +/* * @name dracula - * @author dracula * Michael Kaminsky (http://github.com/mkaminsky11) * Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme) */ diff --git a/packages/codemirror/themes/duotoneDark.mjs b/packages/codemirror/themes/duotoneDark.mjs index 057302cb7..f54973e85 100644 --- a/packages/codemirror/themes/duotoneDark.mjs +++ b/packages/codemirror/themes/duotoneDark.mjs @@ -1,6 +1,6 @@ -/** - * @name duotone - * @author Bram de Haan +/* + * duotone + * author Bram de Haan * by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/duotoneLight.mjs b/packages/codemirror/themes/duotoneLight.mjs index f7b0214b4..81aa34848 100644 --- a/packages/codemirror/themes/duotoneLight.mjs +++ b/packages/codemirror/themes/duotoneLight.mjs @@ -1,6 +1,6 @@ -/** - * @name duotone - * @author Bram de Haan +/* + * duotone + * author Bram de Haan * by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/githubDark.mjs b/packages/codemirror/themes/githubDark.mjs index 5187dcc21..477e7d7b8 100644 --- a/packages/codemirror/themes/githubDark.mjs +++ b/packages/codemirror/themes/githubDark.mjs @@ -1,6 +1,3 @@ -/** - * @name github - */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; diff --git a/packages/codemirror/themes/githubLight.mjs b/packages/codemirror/themes/githubLight.mjs index 382c7ac66..2ccec93b6 100644 --- a/packages/codemirror/themes/githubLight.mjs +++ b/packages/codemirror/themes/githubLight.mjs @@ -1,6 +1,3 @@ -/** - * @name github - */ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; diff --git a/packages/codemirror/themes/green-text.mjs b/packages/codemirror/themes/green-text.mjs index bd21f6d3a..08c7a4bbe 100644 --- a/packages/codemirror/themes/green-text.mjs +++ b/packages/codemirror/themes/green-text.mjs @@ -1,5 +1,5 @@ -/** - * @name Atom One +/* + * Atom One * Atom One dark syntax theme * * https://github.com/atom/one-dark-syntax diff --git a/packages/codemirror/themes/gruvboxDark.mjs b/packages/codemirror/themes/gruvboxDark.mjs index 31d7e42c1..67d8bffc1 100644 --- a/packages/codemirror/themes/gruvboxDark.mjs +++ b/packages/codemirror/themes/gruvboxDark.mjs @@ -1,7 +1,6 @@ -/** - * @name gruvbox-dark - * @author morhetz - * Name: Gruvbox +/* + * gruvbox-dark + * author morhetz * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/gruvboxLight.mjs b/packages/codemirror/themes/gruvboxLight.mjs index ffb4d716c..5d4b4ec01 100644 --- a/packages/codemirror/themes/gruvboxLight.mjs +++ b/packages/codemirror/themes/gruvboxLight.mjs @@ -1,7 +1,6 @@ -/** - * @name gruvbox-light - * @author morhetz - * Name: Gruvbox +/* + * gruvbox-light + * author morhetz * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css */ import { tags as t } from '@lezer/highlight'; diff --git a/packages/codemirror/themes/red-text.mjs b/packages/codemirror/themes/red-text.mjs index 9fe7f516a..44dda5dc0 100644 --- a/packages/codemirror/themes/red-text.mjs +++ b/packages/codemirror/themes/red-text.mjs @@ -1,5 +1,5 @@ -/** - * @name Atom One +/* + * Atom One * Atom One dark syntax theme * * https://github.com/atom/one-dark-syntax diff --git a/packages/codemirror/themes/sonic-pink.mjs b/packages/codemirror/themes/sonic-pink.mjs index 976597ecc..5a19555ab 100644 --- a/packages/codemirror/themes/sonic-pink.mjs +++ b/packages/codemirror/themes/sonic-pink.mjs @@ -1,5 +1,5 @@ -/** - * @name Atom One +/* + * Atom One * Atom One dark syntax theme * * https://github.com/atom/one-dark-syntax diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a6c650e03..21c183da9 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -445,6 +445,32 @@ export const { coarse } = registerControl('coarse'); */ export const { drive } = registerControl('drive'); +/** + * Create byte beats with custom expressions + * + * @name byteBeatExpression + * @synonyms bbexpr + * + * @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat + * @example + * s("bytebeat").bbexpr('t*(t>>15^t>>66)') + * + */ +export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr'); + +/** + * Create byte beats with custom expressions + * + * @name byteBeatStartTime + * @synonyms bbst + * + * @param {number | Pattern} byteBeatStartTime in samples (t) + * @example + * note("c3!8".add("{0 0 12 0 7 5 3}%8")).s("bytebeat:5").bbst("<3 1>".mul(10000))._scope() + * + */ +export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', 'bbst'); + /** * Allows you to set the output channels on the interface * @@ -458,6 +484,41 @@ export const { drive } = registerControl('drive'); */ export const { channels, ch } = registerControl('channels', 'ch'); +/** + * controls the pulsewidth of the pulse oscillator + * + * @name pw + * @param {number | Pattern} pulsewidth + * @example + * note("{f a c e}%16").s("pulse").pw(".8:1:.2") + * @example + * n(run(8)).scale("D:pentatonic").s("pulse").pw("0 .75 .5 1") + */ +export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); + +/** + * controls the lfo rate for the pulsewidth of the pulse oscillator + * + * @name pwrate + * @param {number | Pattern} rate + * @example + * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") + + * + */ +export const { pwrate } = registerControl('pwrate'); + +/** + * controls the lfo sweep for the pulsewidth of the pulse oscillator + * + * @name pwsweep + * @param {number | Pattern} sweep + * @example + * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") + * + */ +export const { pwsweep } = registerControl('pwsweep'); + /** * Phaser audio effect that approximates popular guitar pedals. * @@ -1461,7 +1522,7 @@ export const { vowel } = registerControl('vowel'); * @name waveloss */ export const { waveloss } = registerControl('waveloss'); -/* +/** * Noise crackle density * * @name density diff --git a/packages/core/package.json b/packages/core/package.json index 9123259a5..89b047d2a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.1.0", + "version": "1.2.2", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6e294129e..56a6b77d1 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2441,9 +2441,14 @@ const _chunk = function (n, func, pat, back = false, fast = false) { return pat.when(binary_pat, func); }; -export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) { - return _chunk(n, func, pat, false, false); -}); +export const { chunk, slowchunk, slowChunk } = register( + ['chunk', 'slowchunk', 'slowChunk'], + function (n, func, pat) { + return _chunk(n, func, pat, false, false); + }, + true, + true, +); /** * Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles @@ -2455,9 +2460,14 @@ export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', ' * "0 1 2 3".chunkBack(4, x=>x.add(7)) * .scale("A:minor").note() */ -export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) { - return _chunk(n, func, pat, true); -}); +export const { chunkBack, chunkback } = register( + ['chunkBack', 'chunkback'], + function (n, func, pat) { + return _chunk(n, func, pat, true); + }, + true, + true, +); /** * Like `chunk`, but the cycles of the source pattern aren't repeated @@ -2471,9 +2481,14 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun * .fastChunk(4, x => x.color('red')).slow(2) * .scale("C2:major").note() */ -export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) { - return _chunk(n, func, pat, false, true); -}); +export const { fastchunk, fastChunk } = register( + ['fastchunk', 'fastChunk'], + function (n, func, pat) { + return _chunk(n, func, pat, false, true); + }, + true, + true, +); // TODO - redefine elsewhere in terms of mask export const bypass = register( diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index 334551e15..702201fa6 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -156,8 +156,7 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) { return _pick(lookup, pat, true).resetJoin(); }); -/** - /** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). +/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. * @name inhabit * @synonyms pickSqueeze diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 66ac8a5a6..b6ba5d5cc 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -279,9 +279,9 @@ const _rearrangeWith = (ipat, n, pat) => { }; /** - * @name shuffle * Slices a pattern into the given number of parts, then plays those parts in random order. * Each part will be played exactly once per cycle. + * @name shuffle * @example * note("c d e f").sound("piano").shuffle(4) * @example @@ -292,9 +292,9 @@ export const shuffle = register('shuffle', (n, pat) => { }); /** - * @name scramble * Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`, * but parts might be played more than once, or not at all, per cycle. + * @name scramble * @example * note("c d e f").sound("piano").scramble(4) * @example diff --git a/packages/csound/package.json b/packages/csound/package.json index cf71dfbc3..f200cdaf4 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.1.0", + "version": "1.2.3", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/package.json b/packages/draw/package.json index e93f3ed22..51750da2b 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.1.0", + "version": "1.2.2", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 199c5d034..8b91a15e9 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,38 +1,37 @@ { - "name": "@strudel/gamepad", - "version": "1.1.0", - "description": "Gamepad Inputs for strudel", - "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": [ - "titdalcycles", - "strudel", - "pattern", - "livecoding", - "algorave" - ], - "author": "Yuta Nakayama ", - "license": "AGPL-3.0-or-later", - "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" - }, - "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - "@strudel/core": "workspace:*" - }, - "devDependencies": { - "vite": "^6.0.11" - } + "name": "@strudel/gamepad", + "version": "1.2.2", + "description": "Gamepad Inputs for strudel", + "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": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "vite": "^6.0.11" + } } - \ No newline at end of file diff --git a/packages/hs2js/package.json b/packages/hs2js/package.json index 34bb7f368..c650dfc83 100644 --- a/packages/hs2js/package.json +++ b/packages/hs2js/package.json @@ -1,6 +1,7 @@ { "name": "hs2js", - "version": "0.1.0", + "version": "0.2.0", + "private": true, "description": "Experimental Haskell in JavaScript interpreter", "main": "src/index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index c687d5497..4375ebefc 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.1.0", + "version": "1.2.2", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index c7c5f4988..2956b99f3 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.1.0", + "version": "1.2.3", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index d05d60364..5ae0dc242 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.1.0", + "version": "1.2.2", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index cc117a268..850a75bd1 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,38 +1,37 @@ { - "name": "@strudel/motion", - "version": "1.1.0", - "description": "DeviceMotion API for strudel", - "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": [ - "titdalcycles", - "strudel", - "pattern", - "livecoding", - "algorave" - ], - "author": "Yuta Nakayama ", - "license": "AGPL-3.0-or-later", - "bugs": { - "url": "https://github.com/tidalcycles/strudel/issues" - }, - "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - "@strudel/core": "workspace:*" - }, - "devDependencies": { + "name": "@strudel/motion", + "version": "1.2.2", + "description": "DeviceMotion API for strudel", + "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": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { "vite": "^6.0.11" - } + } } - \ No newline at end of file diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index ef5c3c680..9b9cdaa3b 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.1.0", + "version": "1.2.2", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/package.json b/packages/osc/package.json index 5c56bb2e3..7e87c1e1a 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.1.0", + "version": "1.2.2", "description": "OSC messaging for strudel", "main": "osc.mjs", "type": "module", diff --git a/packages/reference/package.json b/packages/reference/package.json index f69aa6536..fd7edadff 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/reference", - "version": "1.1.0", + "version": "1.2.0", "description": "Headless reference of all strudel functions", "main": "index.mjs", "type": "module", @@ -31,8 +31,6 @@ "url": "https://github.com/tidalcycles/strudel/issues" }, "homepage": "https://github.com/tidalcycles/strudel#readme", - "dependencies": { - }, "devDependencies": { "vite": "^6.0.11" } diff --git a/packages/reference/vite.config.js b/packages/reference/vite.config.js index 5df3edc1b..44ff1e405 100644 --- a/packages/reference/vite.config.js +++ b/packages/reference/vite.config.js @@ -1,5 +1,4 @@ import { defineConfig } from 'vite'; -import { dependencies } from './package.json'; import { resolve } from 'path'; // https://vitejs.dev/config/ @@ -11,9 +10,6 @@ export default defineConfig({ formats: ['es'], fileName: (ext) => ({ es: 'index.mjs' })[ext], }, - rollupOptions: { - external: [...Object.keys(dependencies)], - }, target: 'esnext', }, }); diff --git a/packages/repl/package.json b/packages/repl/package.json index 7a48ad3fd..5487ce443 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.1.0", + "version": "1.2.3", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { @@ -46,6 +46,7 @@ }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/repl/vite.config.js b/packages/repl/vite.config.js index 674e0ebb4..c1ef1bf7f 100644 --- a/packages/repl/vite.config.js +++ b/packages/repl/vite.config.js @@ -1,11 +1,12 @@ import { defineConfig } from 'vite'; import { resolve } from 'path'; import replace from '@rollup/plugin-replace'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ base: './', - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/packages/sampler/package.json b/packages/sampler/package.json index 252fe9350..b0f27f86a 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.1.0", + "version": "0.2.0", "description": "", "keywords": [ "tidalcycles", diff --git a/packages/serial/package.json b/packages/serial/package.json index 74cef80ab..bf6e0dcab 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.1.0", + "version": "1.2.2", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index af6d2b969..bf4fa2960 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.1.0", + "version": "1.2.3", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 5fb8439bf..a835f252d 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.1.0", + "version": "1.2.3", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", @@ -32,7 +32,8 @@ }, "homepage": "https://github.com/tidalcycles/strudel#readme", "devDependencies": { - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" }, "dependencies": { "nanostores": "^0.11.3" diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 91c1a4bbb..2dc4832e1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -8,7 +8,7 @@ import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback, _mod } from './util.mjs'; -import workletsUrl from './worklets.mjs?worker&url'; +import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; @@ -18,9 +18,16 @@ export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; let maxPolyphony = DEFAULT_MAX_POLYPHONY; + export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; } + +let multiChannelOrbits = false; +export function setMultiChannelOrbits(bool) { + multiChannelOrbits = bool == true; +} + export const soundMap = map(); export function registerSound(key, onTrigger, data = {}, onPrepare = () => {}) { @@ -28,6 +35,16 @@ export function registerSound(key, onTrigger, data = {}, onPrepare = () => {}) { soundMap.setKey(key, { onTrigger, data, onPrepare }); } +let gainCurveFunc = (val) => val; + +export function applyGainCurve(val) { + return gainCurveFunc(val); +} + +export function setGainCurve(newGainCurveFunc) { + gainCurveFunc = newGainCurveFunc; +} + function aliasBankMap(aliasMap) { // Make all bank keys lower case for case insensitivity for (const key in aliasMap) { @@ -120,6 +137,7 @@ const defaultDefaultValues = { shapevol: 1, distortvol: 1, delay: 0, + byteBeatExpression: '0', delayfeedback: 0.5, delaytime: 0.25, orbit: 1, @@ -184,8 +202,15 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options; + const { + disableWorklets = false, + maxPolyphony, + audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME, + multiChannelOrbits = false, + } = options; + setMaxPolyphony(maxPolyphony); + setMultiChannelOrbits(multiChannelOrbits); if (typeof window === 'undefined') { return; } @@ -266,7 +291,7 @@ export const connectToDestination = (input, channels = [0, 1]) => { }); stereoMix.connect(splitter); channels.forEach((ch, i) => { - splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1)); + splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount); }); }; @@ -279,7 +304,7 @@ export const panic = () => { channelMerger == null; }; -function getDelay(orbit, delaytime, delayfeedback, t) { +function getDelay(orbit, delaytime, delayfeedback, t, channels) { if (delayfeedback > maxfeedback) { //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); } @@ -288,7 +313,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) { const ac = getAudioContext(); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); dly.start?.(t); // for some reason, this throws when audion extension is installed.. - connectToDestination(dly, [0, 1]); + connectToDestination(dly, channels); delays[orbit] = dly; } delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); @@ -296,16 +321,9 @@ function getDelay(orbit, delaytime, delayfeedback, t) { return delays[orbit]; } -function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { - //gain - const ac = getAudioContext(); - const lfoGain = ac.createGain(); - lfoGain.gain.value = sweep * 2; - // centerFrequency = centerFrequency * 2; - // sweep = sweep * 1.5; - - const lfo = getWorklet(ac, 'lfo-processor', { - frequency, +export function getLfo(audioContext, time, end, properties = {}) { + return getWorklet(audioContext, 'lfo-processor', { + frequency: 1, depth: 1, skew: 0, phaseoffset: 0, @@ -313,8 +331,13 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000 end, shape: 1, dcoffset: -0.5, + ...properties, }); - lfo.connect(lfoGain); +} + +function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { + const ac = getAudioContext(); + const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); //filters const numStages = 2; //num of filters in series @@ -345,12 +368,12 @@ function getFilterType(ftype) { let reverbs = {}; let hasChanged = (now, before) => now !== undefined && now !== before; -function getReverb(orbit, duration, fade, lp, dim, ir) { +function getReverb(orbit, duration, fade, lp, dim, ir, channels) { // If no reverb has been created for a given orbit, create one if (!reverbs[orbit]) { const ac = getAudioContext(); const reverb = ac.createReverb(duration, fade, lp, dim, ir); - connectToDestination(reverb, [0, 1]); + connectToDestination(reverb, channels); reverbs[orbit] = reverb; } if ( @@ -417,8 +440,13 @@ export function resetGlobalEffects() { } let activeSoundSources = new Map(); +//music programs/audio gear usually increments inputs/outputs from 1, we need to subtract 1 from the input because the webaudio API channels start at 0 -export const superdough = async (value, t, hapDuration) => { +function mapChannelNumbers(channels) { + return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); +} + +export const superdough = async (value, t, hapDuration, cps) => { const ac = getAudioContext(); t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; let { stretch } = value; @@ -479,7 +507,7 @@ export const superdough = async (value, t, hapDuration) => { bpsustain, bprelease, bandq = getDefaultValue('bandq'), - channels = getDefaultValue('channels'), + //phaser phaserrate: phaser, phaserdepth = getDefaultValue('phaserdepth'), @@ -515,7 +543,18 @@ export const superdough = async (value, t, hapDuration) => { compressorRelease, } = value; - gain = nanFallback(gain, 1); + const orbitChannels = mapChannelNumbers( + multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'), + ); + const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels; + + gain = applyGainCurve(nanFallback(gain, 1)); + postgain = applyGainCurve(postgain); + shapevol = applyGainCurve(shapevol); + distortvol = applyGainCurve(distortvol); + delay = applyGainCurve(delay); + velocity = applyGainCurve(velocity); + gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future const chainID = Math.round(Math.random() * 1000000); @@ -530,9 +569,6 @@ export const superdough = async (value, t, hapDuration) => { activeSoundSources.delete(chainID); } - //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior - channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); - gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future let audioNodes = []; if (bank && s) { @@ -543,7 +579,7 @@ export const superdough = async (value, t, hapDuration) => { // get source AudioNode let sourceNode; if (source) { - sourceNode = source(t, value, hapDuration); + sourceNode = source(t, value, hapDuration, cps); } else if (getSound(s)) { const { onTrigger } = getSound(s); const onEnded = () => { @@ -682,7 +718,7 @@ export const superdough = async (value, t, hapDuration) => { // delay let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - const delyNode = getDelay(orbit, delaytime, delayfeedback, t); + const delyNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels); delaySend = effectSend(post, delyNode, delay); audioNodes.push(delaySend); } @@ -700,7 +736,7 @@ export const superdough = async (value, t, hapDuration) => { } roomIR = await loadBuffer(url, ac, ir, 0); } - const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR); + const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels); reverbSend = effectSend(post, reverbNode, room); audioNodes.push(reverbSend); } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 8bb47bb7d..834e54c6d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,5 @@ import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound, getAudioContext, getLfo } from './superdough.mjs'; import { applyFM, gainNode, @@ -26,6 +26,9 @@ const getFrequencyFromValue = (value) => { return Number(freq); }; function destroyAudioWorkletNode(node) { + if (node == null) { + return; + } node.disconnect(); node.parameters.get('end')?.setValueAtTime(0, 0); } @@ -141,11 +144,100 @@ export function registerSynthSounds() { { prebake: true, type: 'synth' }, ); + registerSound( + 'bytebeat', + (begin, value, onended) => { + const defaultBeats = [ + '(t%255 >= t/255%255)*255', + '(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400', + 't', + 't*(t >> 10^t)', + 't&128', + 't&t>>8', + '((t%255+t%128+t%64+t%32+t%16+t%127.8+t%64.8+t%32.8+t%16.8)/3)', + '((t%64+t%63.8+t%64.15+t%64.35+t%63.5)/1.25)', + '(t&(t>>7)-t)', + '(sin(t*PI/128)*127+127)', + '((t^t/2+t+64*(sin((t*PI/64)+(t*PI/32768))+64))%128*2)', + '((t^t/2+t+64*(cos >> 0))%127.85*2)', + '((t^t/2+t+64)%128*2)', + '(((t * .25)^(t * .25)/100+(t * .25))%128)*2', + '((t^t/2+t+64)%7 * 24)', + ]; + const { n = 0 } = value; + const frequency = getFrequencyFromValue(value); + const { byteBeatExpression = defaultBeats[n % defaultBeats.length], byteBeatStartTime } = value; + + const ac = getAudioContext(); + + let { duration } = value; + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + ); + const holdend = begin + duration; + const end = holdend + release + 0.01; + + let o = getWorklet( + ac, + 'byte-beat-processor', + { + frequency, + begin, + end, + }, + { + outputChannelCount: [2], + }, + ); + + o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency }); + + let envGain = gainNode(1); + envGain = o.connect(envGain); + + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( + ac, + () => { + destroyAudioWorkletNode(o); + envGain.disconnect(); + onended(); + }, + begin, + end, + ); + + return { + node: envGain, + stop: (time) => { + timeoutNode.stop(time); + }, + }; + }, + { prebake: true, type: 'synth' }, + ); + registerSound( 'pulse', (begin, value, onended) => { const ac = getAudioContext(); - let { duration, n: pulsewidth = 0.5 } = value; + let { pwrate, pwsweep } = value; + if (pwsweep == null) { + if (pwrate != null) { + pwsweep = 0.3; + } else { + pwsweep = 0; + } + } + + if (pwrate == null && pwsweep != null) { + pwrate = 1; + } + + let { duration, pw: pulsewidth = 0.5 } = value; const frequency = getFrequencyFromValue(value); const [attack, decay, sustain, release] = getADSRValues( @@ -153,10 +245,8 @@ export function registerSynthSounds() { 'linear', [0.001, 0.05, 0.6, 0.01], ); - const holdend = begin + duration; const end = holdend + release + 0.01; - let o = getWorklet( ac, 'pulse-oscillator', @@ -178,11 +268,16 @@ export function registerSynthSounds() { envGain = o.connect(envGain); getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - + let lfo; + if (pwsweep != 0) { + lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); + lfo.connect(o.parameters.get('pulsewidth')); + } let timeoutNode = webAudioTimeout( ac, () => { destroyAudioWorkletNode(o); + destroyAudioWorkletNode(lfo); envGain.disconnect(); onended(); fm?.stop(); diff --git a/packages/superdough/vite.config.js b/packages/superdough/vite.config.js index d94e37f41..10ac0f06c 100644 --- a/packages/superdough/vite.config.js +++ b/packages/superdough/vite.config.js @@ -1,10 +1,11 @@ import { defineConfig } from 'vite'; import { dependencies } from './package.json'; import { resolve } from 'path'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'index.mjs'), diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 03d3a966f..24f7434a9 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -295,7 +295,7 @@ class LadderProcessor extends AudioWorkletProcessor { cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff > 1 ? 1 : cutoff; - const k = Math.min(8, resonance * 0.4); + const k = Math.min(8, resonance * 0.13); // drive makeup * resonance volume loss makeup let makeupgain = (1 / drive) * Math.min(1.75, 1 + k); @@ -761,3 +761,137 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { } registerProcessor('pulse-oscillator', PulseOscillatorProcessor); + +/** BYTE BEATS */ +const chyx = { + /*bit*/ bitC: function (x, y, z) { + return x & y ? z : 0; + }, + /*bit reverse*/ br: function (x, size = 8) { + if (size > 32) { + throw new Error('br() Size cannot be greater than 32'); + } else { + let result = 0; + for (let idx = 0; idx < size - 0; idx++) { + result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1))); + } + return result; + } + }, + /*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) { + return Math.sin(x / (128 / Math.PI)); + }, + /*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) { + return Math.cos(x / (128 / Math.PI)); + }, + /*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) { + return Math.tan(x / (128 / Math.PI)); + }, + /*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) { + return X.test(t.toString(2)); + }, +}; + +// Create shortened Math functions +let mathParams, byteBeatHelperFuncs; +function getByteBeatFunc(codetext) { + if ((mathParams || byteBeatHelperFuncs) == null) { + mathParams = Object.getOwnPropertyNames(Math); + byteBeatHelperFuncs = mathParams.map((k) => Math[k]); + const chyxNames = Object.getOwnPropertyNames(chyx); + const chyxFuncs = chyxNames.map((k) => chyx[k]); + mathParams.push('int', 'window', ...chyxNames); + byteBeatHelperFuncs.push(Math.floor, globalThis, ...chyxFuncs); + } + return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs); +} + +class ByteBeatProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.port.onmessage = (event) => { + let { codeText } = event.data; + const { byteBeatStartTime } = event.data; + if (byteBeatStartTime != null) { + this.t = 0; + this.initialOffset = Math.floor(byteBeatStartTime); + } + + //Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important + //Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%"))) + codeText = codeText + .trim() + .replace( + /^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/, + (match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')), + ); + + this.func = getByteBeatFunc(codeText); + }; + this.initialOffset = null; + this.t = null; + this.func = null; + } + + static get parameterDescriptors() { + return [ + { + name: 'begin', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + { + name: 'frequency', + defaultValue: 440, + min: Number.EPSILON, + }, + { + name: 'detune', + defaultValue: 0, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + }, + { + name: 'end', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + ]; + } + + process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } + if (currentTime <= params.begin[0]) { + return true; + } + if (currentTime >= params.end[0]) { + return false; + } + if (this.t == null) { + this.t = params.begin[0] * sampleRate; + } + const output = outputs[0]; + for (let i = 0; i < output[0].length; i++) { + const detune = getParamValue(i, params.detune); + const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); + let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset; + const funcValue = this.func(local_t); + let signal = (funcValue & 255) / 127.5 - 1; + const out = signal * 0.2; + + for (let c = 0; c < output.length; c++) { + //prevent speaker blowout via clipping if threshold exceeds + output[c][i] = clamp(out, -0.4, 0.4); + } + this.t = this.t + 1; + } + + return true; // keep the audio processing going + } +} + +registerProcessor('byte-beat-processor', ByteBeatProcessor); diff --git a/packages/tidal/package.json b/packages/tidal/package.json index 1758732d6..8e4871617 100644 --- a/packages/tidal/package.json +++ b/packages/tidal/package.json @@ -1,6 +1,7 @@ { "name": "@strudel/tidal", - "version": "0.1.0", + "version": "0.2.0", + "private": true, "description": "Experimental Tidal Code interpreter", "module": "tidal.mjs", "repository": { diff --git a/packages/tonal/package.json b/packages/tonal/package.json index e642fa4e3..89f02b301 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.1.0", + "version": "1.2.2", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 55140f802..93648bbc7 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.1.0", + "version": "1.2.2", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/vite-plugin-bundle-audioworklet/package.json b/packages/vite-plugin-bundle-audioworklet/package.json new file mode 100644 index 000000000..f803ae065 --- /dev/null +++ b/packages/vite-plugin-bundle-audioworklet/package.json @@ -0,0 +1,16 @@ +{ + "name": "vite-plugin-bundle-audioworklet", + "main": "./vite-plugin-bundle-audioworklet.js", + "version": "0.1.1", + "description": "", + "keywords": [ + "vite", + "audioworklet" + ], + "author": "Felix Roos ", + "license": "MIT", + "type": "module", + "devDependencies": { + "vite": "^6.0.11" + } +} diff --git a/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js b/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js new file mode 100644 index 000000000..4c5634ba4 --- /dev/null +++ b/packages/vite-plugin-bundle-audioworklet/vite-plugin-bundle-audioworklet.js @@ -0,0 +1,48 @@ +import { createLogger, build } from 'vite'; + +const end = '?audioworklet'; + +function bundleAudioWorkletPlugin() /* : PluginOption */ { + let viteConfig /* : UserConfig */; + + return { + name: 'vite-plugin-bundle-audioworklet', + /* apply: 'build', */ + enforce: 'post', + + config(config) { + viteConfig = config; + }, + + async transform(_code, id) { + if (!id.endsWith(end)) { + return; + } + const entry = id.replace(end, ''); + const quietLogger = createLogger(); + quietLogger.info = () => undefined; + + const output = await build({ + configFile: false, + clearScreen: false, + customLogger: quietLogger, + build: { + lib: { + entry, + name: '_', + formats: ['iife'], + }, + write: false, + }, + }); + if (!(output instanceof Array)) { + throw new Error('Expected output to be Array'); + } + const iife = output[0].output[0].code; + const encoded = Buffer.from(iife, 'utf8').toString('base64'); + return `export default "data:text/javascript;base64,${encoded}";`; + }, + }; +} + +export default bundleAudioWorkletPlugin; diff --git a/packages/web/package.json b/packages/web/package.json index b06e5a489..652496fee 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.1.0", + "version": "1.2.3", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { @@ -41,6 +41,7 @@ }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", - "vite": "^6.0.11" + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js index 99b93956f..671bf2825 100644 --- a/packages/web/vite.config.js +++ b/packages/web/vite.config.js @@ -2,11 +2,12 @@ import { defineConfig } from 'vite'; import { dependencies } from './package.json'; import { resolve } from 'path'; import replace from '@rollup/plugin-replace'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; // https://vitejs.dev/config/ export default defineConfig({ base: './', - plugins: [], + plugins: [bundleAudioWorkletPlugin()], build: { lib: { entry: resolve(__dirname, 'web.mjs'), diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 81b75c036..5714fddf3 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.1.0", + "version": "1.2.3", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 74c3aba73..76fecf525 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.1.0", + "version": "1.2.2", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32fd711e2..a23c80924 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: version: 2.2.7 '@vitest/coverage-v8': specifier: 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)) + version: 3.0.4(vitest@3.0.4) '@vitest/ui': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -75,6 +75,9 @@ importers: prettier: specifier: ^3.4.2 version: 3.4.2 + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:packages/vite-plugin-bundle-audioworklet vitest: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) @@ -428,6 +431,9 @@ importers: 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 packages/sampler: dependencies: @@ -476,6 +482,9 @@ importers: 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 packages/tidal: dependencies: @@ -540,6 +549,12 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/vite-plugin-bundle-audioworklet: + 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) + packages/web: dependencies: '@strudel/core': @@ -564,6 +579,9 @@ importers: 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 packages/webaudio: dependencies: @@ -780,6 +798,9 @@ importers: sharp: specifier: ^0.33.5 version: 0.33.5 + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../packages/vite-plugin-bundle-audioworklet workbox-window: specifier: ^7.3.0 version: 7.3.0 @@ -7561,7 +7582,6 @@ packages: workbox-google-analytics@7.0.0: 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: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} @@ -10248,7 +10268,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@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))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 2d88d8355..34914664d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1516,6 +1516,52 @@ exports[`runs examples > example "brandBy" example index 0 1`] = ` ] `; +exports[`runs examples > example "byteBeatExpression" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", + "[ 1/1 → 2/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", + "[ 2/1 → 3/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", + "[ 3/1 → 4/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]", +] +`; + +exports[`runs examples > example "byteBeatStartTime" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/8 → 1/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/4 → 3/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 3/8 → 1/2 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/2 → 5/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 5/8 → 3/4 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 3/4 → 7/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 7/8 → 1/1 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 1/1 → 9/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 9/8 → 5/4 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 5/4 → 11/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 11/8 → 3/2 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 3/2 → 13/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 13/8 → 7/4 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 7/4 → 15/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 15/8 → 2/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 2/1 → 17/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 17/8 → 9/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 9/4 → 19/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 19/8 → 5/2 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 5/2 → 21/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 21/8 → 11/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 11/4 → 23/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 23/8 → 3/1 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]", + "[ 3/1 → 25/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 25/8 → 13/4 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 13/4 → 27/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 27/8 → 7/2 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 7/2 → 29/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 29/8 → 15/4 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 15/4 → 31/8 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]", + "[ 31/8 → 4/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]", +] +`; + exports[`runs examples > example "cat" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:e5 ]", @@ -2403,6 +2449,27 @@ exports[`runs examples > example "delaytime" example index 0 1`] = ` ] `; +exports[`runs examples > example "density" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:crackle density:0.01 ]", + "[ 1/4 → 1/2 | s:crackle density:0.01 ]", + "[ 1/2 → 3/4 | s:crackle density:0.01 ]", + "[ 3/4 → 1/1 | s:crackle density:0.01 ]", + "[ 1/1 → 5/4 | s:crackle density:0.01 ]", + "[ 5/4 → 3/2 | s:crackle density:0.01 ]", + "[ 3/2 → 7/4 | s:crackle density:0.01 ]", + "[ 7/4 → 2/1 | s:crackle density:0.01 ]", + "[ 2/1 → 9/4 | s:crackle density:0.01 ]", + "[ 9/4 → 5/2 | s:crackle density:0.01 ]", + "[ 5/2 → 11/4 | s:crackle density:0.01 ]", + "[ 11/4 → 3/1 | s:crackle density:0.01 ]", + "[ 3/1 → 13/4 | s:crackle density:0.01 ]", + "[ 13/4 → 7/2 | s:crackle density:0.01 ]", + "[ 7/2 → 15/4 | s:crackle density:0.01 ]", + "[ 15/4 → 4/1 | s:crackle density:0.01 ]", +] +`; + exports[`runs examples > example "detune" example index 0 1`] = ` [ "[ 0/1 → 1/12 | note:d s:supersaw detune:0.1 ]", @@ -6721,6 +6788,186 @@ exports[`runs examples > example "pure" example index 0 1`] = ` ] `; +exports[`runs examples > example "pw" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/16 → 1/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/8 → 3/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/16 → 1/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/4 → 5/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/16 → 3/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/8 → 7/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/16 → 1/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/2 → 9/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 9/16 → 5/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/8 → 11/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 11/16 → 3/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/4 → 13/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 13/16 → 7/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/8 → 15/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 15/16 → 1/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 1/1 → 17/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 17/16 → 9/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 9/8 → 19/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 19/16 → 5/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/4 → 21/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 21/16 → 11/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 11/8 → 23/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 23/16 → 3/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/2 → 25/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 25/16 → 13/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 13/8 → 27/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 27/16 → 7/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/4 → 29/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 29/16 → 15/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 15/8 → 31/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 31/16 → 2/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 2/1 → 33/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 33/16 → 17/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 17/8 → 35/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 35/16 → 9/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 9/4 → 37/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 37/16 → 19/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 19/8 → 39/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 39/16 → 5/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 5/2 → 41/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 41/16 → 21/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 21/8 → 43/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 43/16 → 11/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 11/4 → 45/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 45/16 → 23/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 23/8 → 47/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 47/16 → 3/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 3/1 → 49/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 49/16 → 25/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 25/8 → 51/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 51/16 → 13/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 13/4 → 53/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 53/16 → 27/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 27/8 → 55/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 55/16 → 7/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 7/2 → 57/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 57/16 → 29/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 29/8 → 59/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 59/16 → 15/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 15/4 → 61/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 61/16 → 31/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 31/8 → 63/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", + "[ 63/16 → 4/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]", +] +`; + +exports[`runs examples > example "pw" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | note:D3 s:pulse pw:0 ]", + "[ 1/8 → 1/4 | note:E3 s:pulse pw:0 ]", + "[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 3/8 → 1/2 | note:A3 s:pulse pw:0.75 ]", + "[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 ]", + "[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 ]", + "[ 3/4 → 7/8 | note:E4 s:pulse pw:1 ]", + "[ 7/8 → 1/1 | note:F#4 s:pulse pw:1 ]", + "[ 1/1 → 9/8 | note:D3 s:pulse pw:0 ]", + "[ 9/8 → 5/4 | note:E3 s:pulse pw:0 ]", + "[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 11/8 → 3/2 | note:A3 s:pulse pw:0.75 ]", + "[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 ]", + "[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 ]", + "[ 7/4 → 15/8 | note:E4 s:pulse pw:1 ]", + "[ 15/8 → 2/1 | note:F#4 s:pulse pw:1 ]", + "[ 2/1 → 17/8 | note:D3 s:pulse pw:0 ]", + "[ 17/8 → 9/4 | note:E3 s:pulse pw:0 ]", + "[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 19/8 → 5/2 | note:A3 s:pulse pw:0.75 ]", + "[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 ]", + "[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 ]", + "[ 11/4 → 23/8 | note:E4 s:pulse pw:1 ]", + "[ 23/8 → 3/1 | note:F#4 s:pulse pw:1 ]", + "[ 3/1 → 25/8 | note:D3 s:pulse pw:0 ]", + "[ 25/8 → 13/4 | note:E3 s:pulse pw:0 ]", + "[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.75 ]", + "[ 27/8 → 7/2 | note:A3 s:pulse pw:0.75 ]", + "[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 ]", + "[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 ]", + "[ 15/4 → 31/8 | note:E4 s:pulse pw:1 ]", + "[ 31/8 → 4/1 | note:F#4 s:pulse pw:1 ]", +] +`; + +exports[`runs examples > example "pwrate" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", +] +`; + +exports[`runs examples > example "pwsweep" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]", + "[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]", + "[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]", + "[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", + "[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]", +] +`; + exports[`runs examples > example "queryArc" example index 0 1`] = `[]`; exports[`runs examples > example "rand" example index 0 1`] = ` @@ -7821,9 +8068,7 @@ exports[`runs examples > example "scope" example index 0 1`] = ` ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 0 1`] = ` +exports[`runs examples > example "scramble" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -7844,9 +8089,7 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; -exports[`runs examples > example "scramble -Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`, -but parts might be played more than once, or not at all, per cycle." example index 1 1`] = ` +exports[`runs examples > example "scramble" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", @@ -8301,9 +8544,7 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 0 1`] = ` +exports[`runs examples > example "shuffle" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", @@ -8324,9 +8565,7 @@ Each part will be played exactly once per cycle." example index 0 1`] = ` ] `; -exports[`runs examples > example "shuffle -Slices a pattern into the given number of parts, then plays those parts in random order. -Each part will be played exactly once per cycle." example index 1 1`] = ` +exports[`runs examples > example "shuffle" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:c s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", diff --git a/vitest.config.ts b/vitest.config.mjs similarity index 71% rename from vitest.config.ts rename to vitest.config.mjs index 026f0b7c6..dcdde1133 100644 --- a/vitest.config.ts +++ b/vitest.config.mjs @@ -1,7 +1,9 @@ -import { configDefaults, defineConfig } from 'vitest/config'; +import { defineConfig } from 'vitest/config'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; /// export default defineConfig({ + plugins: [bundleAudioWorkletPlugin()], test: { reporters: 'verbose', isolate: false, diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 631584220..151c48fc1 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -5,6 +5,7 @@ import remarkToc from 'remark-toc'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeUrls from 'rehype-urls'; +import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; import tailwind from '@astrojs/tailwind'; import AstroPWA from '@vite-pwa/astro'; @@ -134,6 +135,7 @@ export default defineConfig({ site, base, vite: { + plugins: [bundleAudioWorkletPlugin()], ssr: { // Example: Force a broken package to skip SSR processing, if needed // external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51 diff --git a/website/package.json b/website/package.json index 257ff6430..8e980f1ca 100644 --- a/website/package.json +++ b/website/package.json @@ -73,6 +73,7 @@ "@vite-pwa/astro": "^0.5.0", "html-escaper": "^3.0.3", "sharp": "^0.33.5", - "workbox-window": "^7.3.0" + "workbox-window": "^7.3.0", + "vite-plugin-bundle-audioworklet": "workspace:*" } } diff --git a/website/src/pages/functions/intro.mdx b/website/src/pages/functions/intro.mdx index 412c086a1..f3e8bec4c 100644 --- a/website/src/pages/functions/intro.mdx +++ b/website/src/pages/functions/intro.mdx @@ -23,7 +23,7 @@ is equivalent to this Pattern without Mini Notation: Similarly, there is an equivalent function for every aspect of the mini notation. Which representation to use is a matter of context. As a rule of thumb, functions -are better suited in a larger context, while mini notation is more practical for individiual rhythms. +are better suited in a larger context, while mini notation is more practical for individual rhythms. ## Limits of Mini Notation diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 442596174..890de3aae 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -66,7 +66,7 @@ While it saves resources, it can also lead to sounds not being audible the first # Sound Banks -If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc.. +If we open the `sounds` tab and then `drum-machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc.. We _could_ use them like this: @@ -180,11 +180,12 @@ It assumes a `strudel.json` file to be present at the root of the repository: https://raw.githubusercontent.com////strudel.json ``` -## From Disk via "Import Sounds" +## From Disk via "Import Sounds Folder" If you don't want to upload your samples to the internet, you can also load them from your local disk. -Go to the `sounds` tab in the REPL and press "import sounds". -Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files. +Go to the `sounds` tab in the REPL and open the `import-sounds` tab below the search bar. +Press the "import sounds folder" button and select a folder that contains audio files. +The folder you select can also contain subfolders with audio files. Example: ``` @@ -200,12 +201,13 @@ Example: ``` In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files. -If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)` +If you select that `samples` folder, the `user` tab (next to the `import-sounds` tab) will then contain 2 new sounds: `swoop(3) smash(3)` The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`. +The samples within each sound use zero-based indexing in alphabetical order. ## From Disk via @strudel/sampler -Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server. +Instead of loading your samples into your browser with the "import sounds folder" button, you can also serve the samples from a local file server. The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler): ```sh diff --git a/website/src/repl/components/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx index 7862b7669..5574c396c 100644 --- a/website/src/repl/components/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -20,25 +20,42 @@ export default function ImportSoundsButton({ onComplete }) { }); return ( - +
+ +
); } diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index c2faf07fa..5be9b602e 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -6,7 +6,7 @@ import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { confirmDialog } from '../../util.mjs'; -import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony } from '@strudel/webaudio'; +import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; function Checkbox({ label, value, onChange, disabled = false }) { return ( @@ -108,6 +108,7 @@ export function SettingsTab({ started }) { audioEngineTarget, togglePanelTrigger, maxPolyphony, + multiChannelOrbits, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -162,6 +163,22 @@ export function SettingsTab({ started }) { value={maxPolyphony ?? ''} /> + + { + const val = cbEvent.target.checked; + confirmDialog(RELOAD_MSG).then((r) => { + if (r == true) { + settingsMap.setKey('multiChannelOrbits', val); + setMultiChannelOrbits(val); + return window.location.reload(); + } + }); + }} + value={multiChannelOrbits} + /> + settingsMap.setKey('theme', theme)} /> diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index c91721e00..a976eb3d2 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -14,6 +14,8 @@ export function SoundsTab() { const sounds = useStore(soundMap); const { soundsFilter } = useSettings(); const [search, setSearch] = useState(''); + const { BASE_URL } = import.meta.env; + const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; const soundEntries = useMemo(() => { if (!sounds) { @@ -37,6 +39,9 @@ export function SoundsTab() { if (soundsFilter === 'synths') { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + if (soundsFilter === 'importSounds') { + return []; + } return filtered; }, [sounds, soundsFilter, search]); @@ -51,7 +56,6 @@ export function SoundsTab() { ref?.stop(getAudioContext().currentTime + 0.01); }); }); - return (
setSearch(v)} /> @@ -65,9 +69,9 @@ export function SoundsTab() { drums: 'drum-machines', synths: 'Synths', user: 'User', + importSounds: 'import-sounds', }} > - settingsMap.setKey('soundsFilter', 'user')} />
@@ -101,7 +105,55 @@ export function SoundsTab() { ); })} - {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''} + {!soundEntries.length && soundsFilter === 'importSounds' ? ( +
+ settingsMap.setKey('soundsFilter', 'user')} /> +

+ To import sounds into strudel, they must be contained{' '} + + within a folder or subfolder + + . The best way to do this is to upload a “samples” folder containing subfolders of individual sounds or + soundbanks (see diagram below).{' '} +

+
+              {`└─ samples <-- import this folder
+   ├─ swoop
+   │  ├─ swoopshort.wav
+   │  ├─ swooplong.wav
+   │  └─ swooptight.wav
+   └─ smash
+      ├─ smashhigh.wav
+      ├─ smashlow.wav
+      └─ smashmiddle.wav`}
+            
+

+ The name of a subfolder corresponds to the sound name under the “user” tab. Multiple samples within a + subfolder are all labelled with the same name, but can be accessed using “.n( )” - remember sounds are + zero-indexed and in alphabetical order! +

+

+ For more information, and other ways to use your own sounds in strudel,{' '} + + check out the docs + + ! +

+

Preview Sounds

+
+              n("0 1 2 3 4 5").s("sample-name")
+            
+

+ Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your + sample as it appears under the "user" tab. +

+
+ ) : ( + '' + )} + {!soundEntries.length && soundsFilter !== 'importSounds' + ? 'No custom sounds loaded in this pattern (yet).' + : ''}
); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 70ccd8f8f..5fc62c576 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -54,25 +54,23 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; const blob = soundFile.blob; - // Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted - if (typeof blob === 'string') { - const soundPaths = sounds.get(parentDirectory) ?? new Set(); - soundPaths.add(blob); - sounds.set(parentDirectory, soundPaths); - return; - } - return blobToDataUrl(blob).then((soundPath) => { - const soundPaths = sounds.get(parentDirectory) ?? new Set(); - soundPaths.add(soundPath); - sounds.set(parentDirectory, soundPaths); + const titlePathMap = sounds.get(parentDirectory) ?? new Map(); + + titlePathMap.set(title, soundPath); + + sounds.set(parentDirectory, titlePathMap); return; }); }), ) .then(() => { - sounds.forEach((soundPaths, key) => { - const value = Array.from(soundPaths); + sounds.forEach((titlePathMap, key) => { + const value = Array.from(titlePathMap.keys()) + .sort((a, b) => { + return a.localeCompare(b); + }) + .map((title) => titlePathMap.get(title)); registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { type: 'sample', diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 9348fa8c1..3ccf41d79 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -19,7 +19,7 @@ import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { clearHydra } from '@strudel/hydra'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { settingsMap, useSettings } from '../settings.mjs'; +import { parseBoolean, settingsMap, useSettings } from '../settings.mjs'; import { setActivePattern, setLatestCode, @@ -37,11 +37,15 @@ import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; -const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get(); +const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName }); + audioReady = initAudioOnFirstClick({ + maxPolyphony, + audioDeviceName, + multiChannelOrbits: parseBoolean(multiChannelOrbits), + }); modulesLoading = loadModules(); presets = prebake(); drawContext = getDrawContext(); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 1b4ba33f9..84b433141 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -38,6 +38,7 @@ export const defaultSettings = { isButtonRowHidden: false, isCSSAnimationDisabled: false, maxPolyphony: 128, + multiChannelOrbits: false, }; let search = null; @@ -50,7 +51,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`; export const settingsMap = persistentMap(settings_key, defaultSettings); -const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); +export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); export function useSettings() { const state = useStore(settingsMap); @@ -81,6 +82,7 @@ export function useSettings() { isPanelPinned: parseBoolean(state.isPanelPinned), isPanelOpen: parseBoolean(state.isPanelOpen), userPatterns: userPatterns, + multiChannelOrbits: parseBoolean(state.multiChannelOrbits), }; }