Compare commits

..

51 Commits

Author SHA1 Message Date
Felix Roos 9bd7b66228 Publish
- @strudel/codemirror@1.2.1
 - @strudel/core@1.2.1
 - @strudel/csound@1.2.2
 - @strudel/draw@1.2.1
 - @strudel/gamepad@1.2.1
 - @strudel/hydra@1.2.1
 - @strudel/midi@1.2.2
 - @strudel/mini@1.2.1
 - @strudel/motion@1.2.1
 - @strudel/mqtt@1.2.1
 - @strudel/osc@1.2.1
 - @strudel/repl@1.2.2
 - @strudel/serial@1.2.1
 - @strudel/soundfonts@1.2.2
 - superdough@1.2.2
 - @strudel/tonal@1.2.1
 - @strudel/transpiler@1.2.1
 - @strudel/web@1.2.2
 - @strudel/webaudio@1.2.2
 - @strudel/xen@1.2.1
2025-05-24 11:30:02 +02:00
Nachito 5c3501caa4 fix: typo on docs causing problems with autocompletion. (#1350)
* fix: scramble and shuffle comments

* fix: comments on themes,docs, tests and more
2025-05-18 20:43:04 +01:00
Jade (Rose) Rowland 1341bd9c92 Merge pull request #1343 from daslyfe/pw
feat: Create pulsewidth (pw) and pulsewidth lfo parameters
2025-05-14 19:12:06 -04:00
Jade (Rose) Rowland 760d1abec0 remove n 2025-05-14 19:10:59 -04:00
Jade (Rose) Rowland 2b44fc48a4 Merge pull request #1344 from daslyfe/multichannelorbits
feat: Multi Channel Orbits
2025-05-14 12:25:16 -04:00
Henré Botha 99779171f5 Fix typo (#1346) 2025-05-13 16:45:29 +01:00
Jade (Rose) Rowland 64ebfb57c3 lint 2025-05-07 11:30:42 -04:00
Jade (Rose) Rowland 099d321dbd format 2025-05-07 11:25:34 -04:00
Jade (Rose) Rowland 59bdfd2cb8 format 2025-05-07 11:12:05 -04:00
Jade (Rose) Rowland 56d3436fda asconst 2025-05-07 01:08:00 -04:00
Jade (Rose) Rowland 440ce0cd3b format 2025-05-06 02:14:35 -04:00
Jade (Rose) Rowland 98346261ed lint 2025-05-06 01:13:50 -04:00
Jade (Rose) Rowland 38e4718826 lint 2025-05-06 01:09:20 -04:00
Jade (Rose) Rowland 38908baccd working 2025-05-06 01:06:37 -04:00
Felix Roos 8d02d4270a Publish
- @strudel/repl@1.2.1
 - @strudel/web@1.2.1
2025-05-02 00:03:18 +02:00
Felix Roos 9cf1cd2656 revert web + repl versions for next try 2025-05-02 00:02:50 +02:00
Felix Roos 4c079b6ebe Merge pull request #1339 from tidalcycles/fix-web-pkg-build
Fix web + repl package builds
2025-05-02 00:01:27 +02:00
Felix Roos e9e2e881e5 fix: repl package build 2025-05-01 23:58:49 +02:00
Felix Roos 685b4f172b fix: duplicate key 2025-05-01 23:57:09 +02:00
Felix Roos 53e50d81b0 also use worklet plugin in web package 2025-05-01 23:56:29 +02:00
Felix Roos 8f58d5788f Publish
- @strudel/csound@1.2.1
 - @strudel/midi@1.2.1
 - @strudel/repl@1.2.1
 - @strudel/soundfonts@1.2.1
 - superdough@1.2.1
 - vite-plugin-bundle-audioworklet@0.1.1
 - @strudel/web@1.2.1
 - @strudel/webaudio@1.2.1
2025-05-01 23:54:13 +02:00
Felix Roos 306a6db8dc hotfix: remove broken tauri workflow 2025-05-01 23:52:36 +02:00
Felix Roos 757d78252b Merge pull request #1338 from tidalcycles/fix-worklet-bundling
fix: superdough worklets bundling
2025-05-01 23:50:56 +02:00
Felix Roos 6f2e026e4a fix: import in vitest config 2025-05-01 23:49:47 +02:00
Felix Roos 22f114be59 update metadata 2025-05-01 23:35:20 +02:00
Felix Roos f993056fb1 fix: tests -> use vite plugin in tests as well 2025-05-01 23:30:14 +02:00
Felix Roos fdbbcd2dd2 add vite-plugin-bundle-audioworklet and use it both from website and superdough 2025-05-01 23:26:04 +02:00
Felix Roos 7bec92ca99 fix: superdough worklets bundling 2025-05-01 23:13:32 +02:00
Felix Roos 5702914661 Publish
- @strudel/reference@1.2.0
2025-05-01 12:52:12 +02:00
Felix Roos f2a9e2d4e6 hotfix: downgrade reference to be able to get 1.2.0 2025-05-01 12:51:54 +02:00
Felix Roos 6398b5fb38 hotfix: make broken hs2js + tidal packages private to opt out of release
- some issue with the build command
2025-05-01 12:48:43 +02:00
Felix Roos 9ceebd4679 hotfix: reference package vite config 2025-05-01 12:47:56 +02:00
Felix Roos 177f5c1431 Publish
- @strudel/codemirror@1.2.0
 - @strudel/core@1.2.0
 - @strudel/csound@1.2.0
 - @strudel/draw@1.2.0
 - @strudel/gamepad@1.2.0
 - hs2js@0.2.0
 - @strudel/hydra@1.2.0
 - @strudel/midi@1.2.0
 - @strudel/mini@1.2.0
 - @strudel/motion@1.2.0
 - @strudel/mqtt@1.2.0
 - @strudel/osc@1.2.0
 - @strudel/reference@1.2.0
 - @strudel/repl@1.2.0
 - @strudel/sampler@0.2.0
 - @strudel/serial@1.2.0
 - @strudel/soundfonts@1.2.0
 - superdough@1.2.0
 - @strudel/tidal@0.2.0
 - @strudel/tonal@1.2.0
 - @strudel/transpiler@1.2.0
 - @strudel/web@1.2.0
 - @strudel/webaudio@1.2.0
 - @strudel/xen@1.2.0
2025-05-01 12:39:24 +02:00
Jade (Rose) Rowland d6c2a88496 Merge pull request #1333 from daslyfe/jade/soundimportorder
FIX: sound import order
2025-04-27 16:25:53 -04:00
Jade (Rose) Rowland 3ee7d0cd4f lint 2025-04-27 16:21:14 -04:00
Jade (Rose) Rowland 53d43dc44c working 2025-04-27 16:19:58 -04:00
Jade (Rose) Rowland 637c619345 Merge pull request #1332 from hpunq/import-sounds-docs
update docs to reflect import sounds tab change
2025-04-27 12:39:49 -04:00
Jade (Rose) Rowland 47fb441feb Merge pull request #1318 from daslyfe/jade/gaincurve
feat: Improve gain curve
2025-04-27 12:38:26 -04:00
hpunq d02b1fc05c update docs to reflect import sounds tab change 2025-04-27 11:05:06 +01:00
Jade (Rose) Rowland 480b5ff58b fix vel 2025-04-27 02:38:10 -04:00
Jade (Rose) Rowland c088c0c9d2 def 2025-04-27 02:21:49 -04:00
Jade (Rose) Rowland f32abac06f reset gaincurve 2025-04-27 02:20:53 -04:00
Jade (Rose) Rowland 697f0a82e6 Merge branch 'main' into jade/gaincurve 2025-04-27 02:10:30 -04:00
Jade (Rose) Rowland 98970417c3 Merge pull request #1331 from tidalcycles/BRANCH_NAME
Add Icon to import sample button
2025-04-27 01:54:49 -04:00
Jade (Rose) Rowland b5d48b2f0b Merge pull request #1329 from hpunq/sample-ui
Add new "import-sounds" tab with explanation on folder import
2025-04-27 01:53:40 -04:00
Jade (Rose) Rowland c48aba50f0 button size and icon 2025-04-27 01:51:41 -04:00
hpunq f539930509 formatted code with prettier 2025-04-26 01:04:11 +01:00
hpunq 759cdb6276 add new import sounds tab with explanation and hyperlinks 2025-04-24 13:53:42 +01:00
hpunq 3dae2a80ab change import sounds label to import sounds folder 2025-04-24 13:51:44 +01:00
Jade (Rose) Rowland 999c0b2349 lint 2025-03-31 00:21:01 -04:00
Jade (Rose) Rowland 5ed28d6168 gain curve func 2025-03-31 00:17:26 -04:00
68 changed files with 790 additions and 366 deletions
-62
View File
@@ -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
+2 -1
View File
@@ -71,6 +71,7 @@
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"lerna": "^8.1.9", "lerna": "^8.1.9",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"vitest": "^3.0.4" "vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/codemirror", "name": "@strudel/codemirror",
"version": "1.1.0", "version": "1.2.1",
"description": "Codemirror Extensions for Strudel", "description": "Codemirror Extensions for Strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+2 -2
View File
@@ -1,5 +1,5 @@
/** /*
* @name Cutie Pi * Cutie Pi
* by Switch Angel * by Switch Angel
*/ */
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
+2 -2
View File
@@ -1,5 +1,5 @@
/** /*
* @name androidstudio * androidstudio
*/ */
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
+2 -2
View File
@@ -1,5 +1,5 @@
/** /*
* @name Atom One * Atom One
* Atom One dark syntax theme * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * https://github.com/atom/one-dark-syntax
+2 -3
View File
@@ -1,6 +1,5 @@
/** /*
* @name darcula * darcula
* @author darcula
* Name: IntelliJ IDEA darcula theme * Name: IntelliJ IDEA darcula theme
* From IntelliJ IDEA by JetBrains * From IntelliJ IDEA by JetBrains
*/ */
+1 -2
View File
@@ -1,6 +1,5 @@
/** /*
* @name dracula * @name dracula
* @author dracula
* Michael Kaminsky (http://github.com/mkaminsky11) * Michael Kaminsky (http://github.com/mkaminsky11)
* Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme) * Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
*/ */
+3 -3
View File
@@ -1,6 +1,6 @@
/** /*
* @name duotone * duotone
* @author Bram de Haan * author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) * 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'; import { tags as t } from '@lezer/highlight';
+3 -3
View File
@@ -1,6 +1,6 @@
/** /*
* @name duotone * duotone
* @author Bram de Haan * author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes) * 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'; import { tags as t } from '@lezer/highlight';
-3
View File
@@ -1,6 +1,3 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
-3
View File
@@ -1,6 +1,3 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
+2 -2
View File
@@ -1,5 +1,5 @@
/** /*
* @name Atom One * Atom One
* Atom One dark syntax theme * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * https://github.com/atom/one-dark-syntax
+3 -4
View File
@@ -1,7 +1,6 @@
/** /*
* @name gruvbox-dark * gruvbox-dark
* @author morhetz * author morhetz
* Name: Gruvbox
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css
*/ */
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
+3 -4
View File
@@ -1,7 +1,6 @@
/** /*
* @name gruvbox-light * gruvbox-light
* @author morhetz * author morhetz
* Name: Gruvbox
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css
*/ */
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
+2 -2
View File
@@ -1,5 +1,5 @@
/** /*
* @name Atom One * Atom One
* Atom One dark syntax theme * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * https://github.com/atom/one-dark-syntax
+2 -2
View File
@@ -1,5 +1,5 @@
/** /*
* @name Atom One * Atom One
* Atom One dark syntax theme * Atom One dark syntax theme
* *
* https://github.com/atom/one-dark-syntax * https://github.com/atom/one-dark-syntax
+36 -1
View File
@@ -458,6 +458,41 @@ export const { drive } = registerControl('drive');
*/ */
export const { channels, ch } = registerControl('channels', 'ch'); 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. * Phaser audio effect that approximates popular guitar pedals.
* *
@@ -1461,7 +1496,7 @@ export const { vowel } = registerControl('vowel');
* @name waveloss * @name waveloss
*/ */
export const { waveloss } = registerControl('waveloss'); export const { waveloss } = registerControl('waveloss');
/* /**
* Noise crackle density * Noise crackle density
* *
* @name density * @name density
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/core", "name": "@strudel/core",
"version": "1.1.0", "version": "1.2.1",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -2
View File
@@ -156,8 +156,7 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin(); 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. * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit * @name inhabit
* @synonyms pickSqueeze * @synonyms pickSqueeze
+2 -2
View File
@@ -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. * 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. * Each part will be played exactly once per cycle.
* @name shuffle
* @example * @example
* note("c d e f").sound("piano").shuffle(4) * note("c d e f").sound("piano").shuffle(4)
* @example * @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`, * 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. * but parts might be played more than once, or not at all, per cycle.
* @name scramble
* @example * @example
* note("c d e f").sound("piano").scramble(4) * note("c d e f").sound("piano").scramble(4)
* @example * @example
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/csound", "name": "@strudel/csound",
"version": "1.1.0", "version": "1.2.2",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/draw", "name": "@strudel/draw",
"version": "1.1.0", "version": "1.2.1",
"description": "Helpers for drawing with Strudel", "description": "Helpers for drawing with Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+35 -36
View File
@@ -1,38 +1,37 @@
{ {
"name": "@strudel/gamepad", "name": "@strudel/gamepad",
"version": "1.1.0", "version": "1.2.1",
"description": "Gamepad Inputs for strudel", "description": "Gamepad Inputs for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git" "url": "git+https://github.com/tidalcycles/strudel.git"
}, },
"keywords": [ "keywords": [
"titdalcycles", "titdalcycles",
"strudel", "strudel",
"pattern", "pattern",
"livecoding", "livecoding",
"algorave" "algorave"
], ],
"author": "Yuta Nakayama <nkymut@gmail.com>", "author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/tidalcycles/strudel/issues" "url": "https://github.com/tidalcycles/strudel/issues"
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*" "@strudel/core": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^6.0.11" "vite": "^6.0.11"
} }
} }
+2 -1
View File
@@ -1,6 +1,7 @@
{ {
"name": "hs2js", "name": "hs2js",
"version": "0.1.0", "version": "0.2.0",
"private": true,
"description": "Experimental Haskell in JavaScript interpreter", "description": "Experimental Haskell in JavaScript interpreter",
"main": "src/index.mjs", "main": "src/index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/hydra", "name": "@strudel/hydra",
"version": "1.1.0", "version": "1.2.1",
"description": "Hydra integration for strudel", "description": "Hydra integration for strudel",
"main": "hydra.mjs", "main": "hydra.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/midi", "name": "@strudel/midi",
"version": "1.1.0", "version": "1.2.2",
"description": "Midi API for strudel", "description": "Midi API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/mini", "name": "@strudel/mini",
"version": "1.1.0", "version": "1.2.1",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+34 -35
View File
@@ -1,38 +1,37 @@
{ {
"name": "@strudel/motion", "name": "@strudel/motion",
"version": "1.1.0", "version": "1.2.1",
"description": "DeviceMotion API for strudel", "description": "DeviceMotion API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
"publishConfig": { "publishConfig": {
"main": "dist/index.mjs" "main": "dist/index.mjs"
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git" "url": "git+https://github.com/tidalcycles/strudel.git"
}, },
"keywords": [ "keywords": [
"titdalcycles", "titdalcycles",
"strudel", "strudel",
"pattern", "pattern",
"livecoding", "livecoding",
"algorave" "algorave"
], ],
"author": "Yuta Nakayama <nkymut@gmail.com>", "author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/tidalcycles/strudel/issues" "url": "https://github.com/tidalcycles/strudel/issues"
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*" "@strudel/core": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^6.0.11" "vite": "^6.0.11"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/mqtt", "name": "@strudel/mqtt",
"version": "1.1.0", "version": "1.2.1",
"description": "MQTT API for strudel", "description": "MQTT API for strudel",
"main": "mqtt.mjs", "main": "mqtt.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/osc", "name": "@strudel/osc",
"version": "1.1.0", "version": "1.2.1",
"description": "OSC messaging for strudel", "description": "OSC messaging for strudel",
"main": "osc.mjs", "main": "osc.mjs",
"type": "module", "type": "module",
+1 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/reference", "name": "@strudel/reference",
"version": "1.1.0", "version": "1.2.0",
"description": "Headless reference of all strudel functions", "description": "Headless reference of all strudel functions",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -31,8 +31,6 @@
"url": "https://github.com/tidalcycles/strudel/issues" "url": "https://github.com/tidalcycles/strudel/issues"
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
},
"devDependencies": { "devDependencies": {
"vite": "^6.0.11" "vite": "^6.0.11"
} }
-4
View File
@@ -1,5 +1,4 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
@@ -11,9 +10,6 @@ export default defineConfig({
formats: ['es'], formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext], fileName: (ext) => ({ es: 'index.mjs' })[ext],
}, },
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext', target: 'esnext',
}, },
}); });
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/repl", "name": "@strudel/repl",
"version": "1.1.0", "version": "1.2.2",
"description": "Strudel REPL as a Web Component", "description": "Strudel REPL as a Web Component",
"module": "index.mjs", "module": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -46,6 +46,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11" "vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+2 -1
View File
@@ -1,11 +1,12 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { resolve } from 'path'; import { resolve } from 'path';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
plugins: [], plugins: [bundleAudioWorkletPlugin()],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/sampler", "name": "@strudel/sampler",
"version": "0.1.0", "version": "0.2.0",
"description": "", "description": "",
"keywords": [ "keywords": [
"tidalcycles", "tidalcycles",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/serial", "name": "@strudel/serial",
"version": "1.1.0", "version": "1.2.1",
"description": "Webserial API for strudel", "description": "Webserial API for strudel",
"main": "serial.mjs", "main": "serial.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/soundfonts", "name": "@strudel/soundfonts",
"version": "1.1.0", "version": "1.2.2",
"description": "Soundsfont support for strudel", "description": "Soundsfont support for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "superdough", "name": "superdough",
"version": "1.1.0", "version": "1.2.2",
"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",
@@ -32,7 +32,8 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": { "devDependencies": {
"vite": "^6.0.11" "vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
}, },
"dependencies": { "dependencies": {
"nanostores": "^0.11.3" "nanostores": "^0.11.3"
+60 -25
View File
@@ -8,7 +8,7 @@ import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.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 { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -18,9 +18,16 @@ export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
let maxPolyphony = DEFAULT_MAX_POLYPHONY; let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) { export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
} }
let multiChannelOrbits = false;
export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
export const soundMap = map(); export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) { export function registerSound(key, onTrigger, data = {}) {
@@ -28,6 +35,16 @@ export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data }); soundMap.setKey(key, { onTrigger, data });
} }
let gainCurveFunc = (val) => val;
export function applyGainCurve(val) {
return gainCurveFunc(val);
}
export function setGainCurve(newGainCurveFunc) {
gainCurveFunc = newGainCurveFunc;
}
function aliasBankMap(aliasMap) { function aliasBankMap(aliasMap) {
// Make all bank keys lower case for case insensitivity // Make all bank keys lower case for case insensitivity
for (const key in aliasMap) { for (const key in aliasMap) {
@@ -184,8 +201,15 @@ function loadWorklets() {
// this function should be called on first user interaction (to avoid console warning) // this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) { 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); setMaxPolyphony(maxPolyphony);
setMultiChannelOrbits(multiChannelOrbits);
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
@@ -266,7 +290,7 @@ export const connectToDestination = (input, channels = [0, 1]) => {
}); });
stereoMix.connect(splitter); stereoMix.connect(splitter);
channels.forEach((ch, i) => { 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 +303,7 @@ export const panic = () => {
channelMerger == null; channelMerger == null;
}; };
function getDelay(orbit, delaytime, delayfeedback, t) { function getDelay(orbit, delaytime, delayfeedback, t, channels) {
if (delayfeedback > maxfeedback) { if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
} }
@@ -288,7 +312,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
const ac = getAudioContext(); const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed.. 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] = dly;
} }
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
@@ -296,16 +320,9 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
return delays[orbit]; return delays[orbit];
} }
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { export function getLfo(audioContext, time, end, properties = {}) {
//gain return getWorklet(audioContext, 'lfo-processor', {
const ac = getAudioContext(); frequency: 1,
const lfoGain = ac.createGain();
lfoGain.gain.value = sweep * 2;
// centerFrequency = centerFrequency * 2;
// sweep = sweep * 1.5;
const lfo = getWorklet(ac, 'lfo-processor', {
frequency,
depth: 1, depth: 1,
skew: 0, skew: 0,
phaseoffset: 0, phaseoffset: 0,
@@ -313,8 +330,13 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
end, end,
shape: 1, shape: 1,
dcoffset: -0.5, 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 //filters
const numStages = 2; //num of filters in series const numStages = 2; //num of filters in series
@@ -345,12 +367,12 @@ function getFilterType(ftype) {
let reverbs = {}; let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before; 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 no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) { if (!reverbs[orbit]) {
const ac = getAudioContext(); const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir); const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, [0, 1]); connectToDestination(reverb, channels);
reverbs[orbit] = reverb; reverbs[orbit] = reverb;
} }
if ( if (
@@ -417,6 +439,11 @@ export function resetGlobalEffects() {
} }
let activeSoundSources = new Map(); 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
function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration) => { export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext(); const ac = getAudioContext();
@@ -479,7 +506,7 @@ export const superdough = async (value, t, hapDuration) => {
bpsustain, bpsustain,
bprelease, bprelease,
bandq = getDefaultValue('bandq'), bandq = getDefaultValue('bandq'),
channels = getDefaultValue('channels'),
//phaser //phaser
phaserrate: phaser, phaserrate: phaser,
phaserdepth = getDefaultValue('phaserdepth'), phaserdepth = getDefaultValue('phaserdepth'),
@@ -515,7 +542,18 @@ export const superdough = async (value, t, hapDuration) => {
compressorRelease, compressorRelease,
} = value; } = 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); const chainID = Math.round(Math.random() * 1000000);
@@ -530,9 +568,6 @@ export const superdough = async (value, t, hapDuration) => {
activeSoundSources.delete(chainID); 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 = []; let audioNodes = [];
if (bank && s) { if (bank && s) {
@@ -682,7 +717,7 @@ export const superdough = async (value, t, hapDuration) => {
// delay // delay
let delaySend; let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { 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); delaySend = effectSend(post, delyNode, delay);
audioNodes.push(delaySend); audioNodes.push(delaySend);
} }
@@ -700,7 +735,7 @@ export const superdough = async (value, t, hapDuration) => {
} }
roomIR = await loadBuffer(url, ac, ir, 0); 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); reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend); audioNodes.push(reverbSend);
} }
+24 -5
View File
@@ -1,5 +1,5 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs'; import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs'; import { registerSound, getAudioContext, getLfo } from './superdough.mjs';
import { import {
applyFM, applyFM,
gainNode, gainNode,
@@ -26,6 +26,9 @@ const getFrequencyFromValue = (value) => {
return Number(freq); return Number(freq);
}; };
function destroyAudioWorkletNode(node) { function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect(); node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0); node.parameters.get('end')?.setValueAtTime(0, 0);
} }
@@ -145,7 +148,20 @@ export function registerSynthSounds() {
'pulse', 'pulse',
(begin, value, onended) => { (begin, value, onended) => {
const ac = getAudioContext(); 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 frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues( const [attack, decay, sustain, release] = getADSRValues(
@@ -153,10 +169,8 @@ export function registerSynthSounds() {
'linear', 'linear',
[0.001, 0.05, 0.6, 0.01], [0.001, 0.05, 0.6, 0.01],
); );
const holdend = begin + duration; const holdend = begin + duration;
const end = holdend + release + 0.01; const end = holdend + release + 0.01;
let o = getWorklet( let o = getWorklet(
ac, ac,
'pulse-oscillator', 'pulse-oscillator',
@@ -178,11 +192,16 @@ export function registerSynthSounds() {
envGain = o.connect(envGain); envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); 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( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
destroyAudioWorkletNode(o); destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
envGain.disconnect(); envGain.disconnect();
onended(); onended();
fm?.stop(); fm?.stop();
+2 -1
View File
@@ -1,10 +1,11 @@
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';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [], plugins: [bundleAudioWorkletPlugin()],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'index.mjs'), entry: resolve(__dirname, 'index.mjs'),
+2 -1
View File
@@ -1,6 +1,7 @@
{ {
"name": "@strudel/tidal", "name": "@strudel/tidal",
"version": "0.1.0", "version": "0.2.0",
"private": true,
"description": "Experimental Tidal Code interpreter", "description": "Experimental Tidal Code interpreter",
"module": "tidal.mjs", "module": "tidal.mjs",
"repository": { "repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/tonal", "name": "@strudel/tonal",
"version": "1.1.0", "version": "1.2.1",
"description": "Tonal functions for strudel", "description": "Tonal functions for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/transpiler", "name": "@strudel/transpiler",
"version": "1.1.0", "version": "1.2.1",
"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",
@@ -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 <flix91@gmail.com>",
"license": "MIT",
"type": "module",
"devDependencies": {
"vite": "^6.0.11"
}
}
@@ -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;
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/web", "name": "@strudel/web",
"version": "1.1.0", "version": "1.2.2",
"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": {
@@ -41,6 +41,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11" "vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+2 -1
View File
@@ -2,11 +2,12 @@ import { defineConfig } from 'vite';
import { dependencies } from './package.json'; import { dependencies } from './package.json';
import { resolve } from 'path'; import { resolve } from 'path';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
plugins: [], plugins: [bundleAudioWorkletPlugin()],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'web.mjs'), entry: resolve(__dirname, 'web.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/webaudio", "name": "@strudel/webaudio",
"version": "1.1.0", "version": "1.2.2",
"description": "Web Audio helpers for Strudel", "description": "Web Audio helpers for Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel/xen", "name": "@strudel/xen",
"version": "1.1.0", "version": "1.2.1",
"description": "Xenharmonic API for strudel", "description": "Xenharmonic API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
+23 -3
View File
@@ -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(@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': '@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)
@@ -75,6 +75,9 @@ importers:
prettier: prettier:
specifier: ^3.4.2 specifier: ^3.4.2
version: 3.4.2 version: 3.4.2
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:packages/vite-plugin-bundle-audioworklet
vitest: vitest:
specifier: ^3.0.4 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) 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: vite:
specifier: ^6.0.11 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) 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: packages/sampler:
dependencies: dependencies:
@@ -476,6 +482,9 @@ importers:
vite: vite:
specifier: ^6.0.11 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) 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: packages/tidal:
dependencies: dependencies:
@@ -540,6 +549,12 @@ importers:
specifier: ^3.0.4 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) 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: packages/web:
dependencies: dependencies:
'@strudel/core': '@strudel/core':
@@ -564,6 +579,9 @@ importers:
vite: vite:
specifier: ^6.0.11 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) 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: packages/webaudio:
dependencies: dependencies:
@@ -780,6 +798,9 @@ importers:
sharp: sharp:
specifier: ^0.33.5 specifier: ^0.33.5
version: 0.33.5 version: 0.33.5
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../packages/vite-plugin-bundle-audioworklet
workbox-window: workbox-window:
specifier: ^7.3.0 specifier: ^7.3.0
version: 7.3.0 version: 7.3.0
@@ -7561,7 +7582,6 @@ 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==}
@@ -10248,7 +10268,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
+249 -6
View File
@@ -2403,6 +2403,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`] = ` exports[`runs examples > example "detune" example index 0 1`] = `
[ [
"[ 0/1 → 1/12 | note:d s:supersaw detune:0.1 ]", "[ 0/1 → 1/12 | note:d s:supersaw detune:0.1 ]",
@@ -6721,6 +6742,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 "queryArc" example index 0 1`] = `[]`;
exports[`runs examples > example "rand" example index 0 1`] = ` exports[`runs examples > example "rand" example index 0 1`] = `
@@ -7871,6 +8072,52 @@ but parts might be played more than once, or not at all, per cycle." example ind
] ]
`; `;
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 ]",
"[ 1/2 → 3/4 | note:d s:piano ]",
"[ 3/4 → 1/1 | note:c s:piano ]",
"[ 1/1 → 5/4 | note:e s:piano ]",
"[ 5/4 → 3/2 | note:e s:piano ]",
"[ 3/2 → 7/4 | note:e s:piano ]",
"[ 7/4 → 2/1 | note:c s:piano ]",
"[ 2/1 → 9/4 | note:f s:piano ]",
"[ 9/4 → 5/2 | note:d s:piano ]",
"[ 5/2 → 11/4 | note:d s:piano ]",
"[ 11/4 → 3/1 | note:e s:piano ]",
"[ 3/1 → 13/4 | note:c s:piano ]",
"[ 13/4 → 7/2 | note:f s:piano ]",
"[ 7/2 → 15/4 | note:d s:piano ]",
"[ 15/4 → 4/1 | note:f s:piano ]",
]
`;
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 ]",
"[ 1/4 → 3/8 | note:d s:piano ]",
"[ 3/8 → 1/2 | note:c s:piano ]",
"[ 1/2 → 1/1 | note:g s:piano ]",
"[ 1/1 → 9/8 | note:e s:piano ]",
"[ 9/8 → 5/4 | note:e s:piano ]",
"[ 5/4 → 11/8 | note:e s:piano ]",
"[ 11/8 → 3/2 | note:c s:piano ]",
"[ 3/2 → 2/1 | note:g s:piano ]",
"[ 2/1 → 17/8 | note:f s:piano ]",
"[ 17/8 → 9/4 | note:d s:piano ]",
"[ 9/4 → 19/8 | note:d s:piano ]",
"[ 19/8 → 5/2 | note:e s:piano ]",
"[ 5/2 → 3/1 | note:g s:piano ]",
"[ 3/1 → 25/8 | note:c s:piano ]",
"[ 25/8 → 13/4 | note:f s:piano ]",
"[ 13/4 → 27/8 | note:d s:piano ]",
"[ 27/8 → 7/2 | note:f s:piano ]",
"[ 7/2 → 4/1 | note:g s:piano ]",
]
`;
exports[`runs examples > example "scrub" example index 0 1`] = ` exports[`runs examples > example "scrub" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", "[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
@@ -8301,9 +8548,7 @@ exports[`runs examples > example "shrink" example index 3 1`] = `
] ]
`; `;
exports[`runs examples > example "shuffle exports[`runs examples > example "shuffle" example index 0 1`] = `
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`] = `
[ [
"[ 0/1 → 1/4 | note:c s:piano ]", "[ 0/1 → 1/4 | note:c s:piano ]",
"[ 1/4 → 1/2 | note:d s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]",
@@ -8324,9 +8569,7 @@ Each part will be played exactly once per cycle." example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "shuffle exports[`runs examples > example "shuffle" example index 1 1`] = `
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`] = `
[ [
"[ 0/1 → 1/8 | note:c s:piano ]", "[ 0/1 → 1/8 | note:c s:piano ]",
"[ 1/8 → 1/4 | note:d s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]",
+3 -1
View File
@@ -1,7 +1,9 @@
import { configDefaults, defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
/// <reference types="vitest" /> /// <reference types="vitest" />
export default defineConfig({ export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
test: { test: {
reporters: 'verbose', reporters: 'verbose',
isolate: false, isolate: false,
+2
View File
@@ -5,6 +5,7 @@ import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug'; import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeUrls from 'rehype-urls'; import rehypeUrls from 'rehype-urls';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import tailwind from '@astrojs/tailwind'; import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro'; import AstroPWA from '@vite-pwa/astro';
@@ -134,6 +135,7 @@ export default defineConfig({
site, site,
base, base,
vite: { vite: {
plugins: [bundleAudioWorkletPlugin()],
ssr: { ssr: {
// Example: Force a broken package to skip SSR processing, if needed // Example: Force a broken package to skip SSR processing, if needed
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51 // external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
+2 -1
View File
@@ -73,6 +73,7 @@
"@vite-pwa/astro": "^0.5.0", "@vite-pwa/astro": "^0.5.0",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"workbox-window": "^7.3.0" "workbox-window": "^7.3.0",
"vite-plugin-bundle-audioworklet": "workspace:*"
} }
} }
+1 -1
View File
@@ -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. 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 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 ## Limits of Mini Notation
+8 -6
View File
@@ -66,7 +66,7 @@ While it saves resources, it can also lead to sounds not being audible the first
# Sound Banks # 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: 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/<user>/<repo>/<branch>/strudel.json https://raw.githubusercontent.com/<user>/<repo>/<branch>/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. 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". Go to the `sounds` tab in the REPL and open the `import-sounds` tab below the search bar.
Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files. 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: Example:
``` ```
@@ -200,12 +201,13 @@ Example:
``` ```
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files. 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 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 ## 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): The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
```sh ```sh
+11 -18
View File
@@ -296,23 +296,16 @@ The whole modulation will now take 8 cycles to repeat.
## Recap ## Recap
| Name | Description | Example | | name | example |
| ------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | ------- | ---------------------------------------------------------------------------------------------------------------- |
| lpf | Low-pass filter, cuts high frequencies | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> | | lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | Vowel-like filter | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> | | vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | Adjusts volume/amplitude | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> | | gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| attack | ADSR: Time to reach full volume | <MiniRepl client:visible tune={`note("c3").s("sawtooth").attack(.5)`} /> | | delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| decay | ADSR: Time to reach sustain level | <MiniRepl client:visible tune={`note("c3").s("sawtooth").decay(.5)`} /> | | room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| sustain | ADSR: Volume level while note is held | <MiniRepl client:visible tune={`note("c3").s("sawtooth").sustain(.5)`} /> | | pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| release | ADSR: Time to fade out after note ends | <MiniRepl client:visible tune={`note("c3").s("sawtooth").release(.5)`} /> | | speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| adsr | ADSR: Short notation (attack:decay:sustain:release) | <MiniRepl client:visible tune={`note("c3").s("sawtooth").adsr(".1:.2:.5:.3")`} /> | | signals | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain (saw)`} /> |
| delay | Creates echoes | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> | | range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
| room | Simulates room reverb | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | Pans sound left (0) to right (1) | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | Changes playback speed (and pitch) | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| fast | Speeds up pattern playback (outside mini-notation) | <MiniRepl client:visible tune={`s("bd sd").fast(2)`} /> |
| slow | Slows down pattern playback (outside mini-notation)| <MiniRepl client:visible tune={`s("bd sd").slow(2)`} /> |
| signals | Modulate parameters with waveforms (`sine`, etc.) | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain(saw)`} /> |
| range | Set min/max values for signal modulation | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects). Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).
+6 -8
View File
@@ -305,14 +305,12 @@ What's the difference?
Let's recap what we've learned in this chapter: Let's recap what we've learned in this chapter:
| Concept | Syntax | Description | Example | | Concept | Syntax | Example |
| --------------- | ------ | ---------------------------------------------- | -------------------------------------------------------------------------- | | --------- | ------ | -------------------------------------------------------- |
| Slow down | / | Divide a sequence over multiple cycles | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> | | Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | Play one element per cycle from a sequence | <MiniRepl client:visible tune={`note("c a f <e g>")`} /> | | Alternate | \<\> | <MiniRepl client:visible tune={`note("c a f <e g>")`} /> |
| Elongate | @ | Extend the duration of an element | <MiniRepl client:visible tune={`note("c@3 e")`} /> | | Elongate | @ | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | Repeat an element multiple times | <MiniRepl client:visible tune={`note("c!3 e")`} /> | | Replicate | ! | <MiniRepl client:visible tune={`note("c!3 e")`} /> |
| Switch sounds | space | Cycle through sounds within the `sound` func | <MiniRepl client:visible tune={`note("c e").sound("piano sawtooth")`} /> |
| Stack sounds | , | Play multiple sounds together in `sound` func | <MiniRepl client:visible tune={`note("c e").sound("piano, sawtooth")`} /> |
New functions: New functions:
+9 -10
View File
@@ -270,17 +270,16 @@ This is shorter and more readable than:
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal. Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
This is what we've learned so far: This is what we've learned so far:
| Concept | Syntax | Description | Example | | Concept | Syntax | Example |
| ----------------- | -------- | ----------------------------------------- | ----------------------------------------------------------------------- | | ----------------- | -------- | ----------------------------------------------------------------------- |
| Sequence | space | Play sounds one after another | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> | | Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
| Sample Number | :x | Select a specific sample from a sound | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> | | Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | - or ~ | Add silence into the sequence | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> | | Rests | - or ~ | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`sound("<bd hh rim oh bd rim>")`} /> | | Alternate | \<\> | <MiniRepl client:visible tune={`sound("<bd hh rim oh bd rim>")`} /> |
| Sub-Sequences | \[\] | Group elements, squished into their space | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> | | Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | Nest groups within groups | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> | | Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz [sd cp]]]")`} /> |
| Speed up | \* | Make elements play faster | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> | | Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | Play multiple sequences at the same time | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> | | Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Multiple-Lines | \`\` | Write sequences across multiple lines | <MiniRepl client:visible tune={'sound(`bd*2, \n hh*4`)'} /> |
The Mini-Notation is usually used inside some function. These are the functions we've seen so far: The Mini-Notation is usually used inside some function. These are the functions we've seen so far:
+24 -31
View File
@@ -11,20 +11,19 @@ This page is just a listing of all functions covered in the workshop!
## Mini Notation ## Mini Notation
| Concept | Syntax | Description | Example | | Concept | Syntax | Example |
| ----------------- | -------- | ----------------------------------------- | ----------------------------------------------------------------------- | | ----------------- | -------- | --------------------------------------------------------------------- |
| Sequence | space | Play sounds one after another | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> | | Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh bd cp sd hh")`} /> |
| Sample Number | :x | Select a specific sample from a sound | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> | | Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | ~ | Add silence into the sequence | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> | | Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | Group elements, squished into their space | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> | | Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | Nest groups within groups | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> | | Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Speed up | \* | Make elements play faster | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> | | Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
| Parallel | , | Play multiple sequences at the same time | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> | | Parallel | , | <MiniRepl client:visible tune={`sound("bd*2, hh*2 [hh oh]")`} /> |
| Slow down | \/ | Divide a sequence over multiple cycles | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> | | Slow down | \/ | <MiniRepl client:visible tune={`note("[c a f e]/2")`} /> |
| Alternate | \<\> | Play one element per cycle from a sequence| <MiniRepl client:visible tune={`note("c <e g>")`} /> | | Alternate | \<\> | <MiniRepl client:visible tune={`note("c <e g>")`} /> |
| Elongate | @ | Extend the duration of an element | <MiniRepl client:visible tune={`note("c@3 e")`} /> | | Elongate | @ | <MiniRepl client:visible tune={`note("c@3 e")`} /> |
| Replicate | ! | Repeat an element multiple times | <MiniRepl client:visible tune={`note("c!3 e")`} /> | | Replicate | ! | <MiniRepl client:visible tune={`note("c!3 e")`} /> |
| Multiple-Lines | \`\` | Write sequences across multiple lines | <MiniRepl client:visible tune={'sound(`bd*2, \n hh*4`)'} /> |
## Sounds ## Sounds
@@ -44,26 +43,20 @@ This page is just a listing of all functions covered in the workshop!
## Audio Effects ## Audio Effects
| Name | Description | Example | | name | example |
| ------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | ----- | --------------------------------------------------------------------------------------- |
| lpf | Low-pass filter, cuts high frequencies | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("400 2000")`} /> | | lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("400 2000")`} /> |
| vowel | Vowel-like filter | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> | | vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | Adjusts volume/amplitude | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> | | gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*4")`} /> |
| attack | ADSR: Time to reach full volume | <MiniRepl client:visible tune={`note("c3").s("sawtooth").attack(.5)`} /> | | delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| decay | ADSR: Time to reach sustain level | <MiniRepl client:visible tune={`note("c3").s("sawtooth").decay(.5)`} /> | | room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| sustain | ADSR: Volume level while note is held | <MiniRepl client:visible tune={`note("c3").s("sawtooth").sustain(.5)`} /> | | pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| release | ADSR: Time to fade out after note ends | <MiniRepl client:visible tune={`note("c3").s("sawtooth").release(.5)`} /> | | speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| adsr | ADSR: Short notation (attack:decay:sustain:release) | <MiniRepl client:visible tune={`note("c3").s("sawtooth").adsr(".1:.2:.5:.3")`} /> | | range | <MiniRepl client:visible tune={`s("hh*32").lpf(saw.range(200,4000))`} /> |
| delay | Creates echoes | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | Simulates room reverb | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | Pans sound left (0) to right (1) | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | Changes playback speed (and pitch) | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| signals | Modulate parameters with waveforms (`sine`, etc.) | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain(saw)`} /> |
| range | Set min/max values for signal modulation | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
## Pattern Effects ## Pattern Effects
| Name | Description | Example | | name | description | example |
| ---- | ----------------------------------- | ----------------------------------------------------------------------------------- | | ---- | ----------------------------------- | ----------------------------------------------------------------------------------- |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").cpm(45)`} /> | | cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").cpm(45)`} /> |
| fast | speed up | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").fast(2)`} /> | | fast | speed up | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").fast(2)`} /> |
@@ -20,25 +20,42 @@ export default function ImportSoundsButton({ onComplete }) {
}); });
return ( return (
<label <div>
style={{ alignItems: 'center' }} <label
className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md hover:opacity-50 whitespace-nowrap cursor-pointer" style={{ alignItems: 'center', borderColor: 'red', border: 1 }}
> className="flex bg-background p-4 w-fit rounded-xl hover:opacity-50 whitespace-nowrap cursor-pointer"
<input >
disabled={isUploading} <svg
ref={fileUploadRef} xmlns="http://www.w3.org/2000/svg"
id="audio_file" fill="none"
style={{ display: 'none' }} viewBox="0 0 24 24"
type="file" strokeWidth={1.5}
directory="" stroke="currentColor"
webkitdirectory="" className="size-6 mr-2"
multiple >
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg" <path
onChange={() => { strokeLinecap="round"
onChange(); strokeLinejoin="round"
}} d="M7.5 7.5h-.75A2.25 2.25 0 0 0 4.5 9.75v7.5a2.25 2.25 0 0 0 2.25 2.25h7.5a2.25 2.25 0 0 0 2.25-2.25v-7.5a2.25 2.25 0 0 0-2.25-2.25h-.75m0-3-3-3m0 0-3 3m3-3v11.25m6-2.25h.75a2.25 2.25 0 0 1 2.25 2.25v7.5a2.25 2.25 0 0 1-2.25 2.25h-7.5a2.25 2.25 0 0 1-2.25-2.25v-.75"
/> />
{isUploading ? 'importing...' : 'import sounds'} </svg>
</label>
<input
disabled={isUploading}
ref={fileUploadRef}
id="audio_file"
style={{ display: 'none' }}
type="file"
directory=""
webkitdirectory=""
multiple
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
onChange={() => {
onChange();
}}
/>
{isUploading ? 'importing...' : 'import sounds folder'}
</label>
</div>
); );
} }
@@ -6,7 +6,7 @@ import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs'; 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 }) { function Checkbox({ label, value, onChange, disabled = false }) {
return ( return (
@@ -108,6 +108,7 @@ export function SettingsTab({ started }) {
audioEngineTarget, audioEngineTarget,
togglePanelTrigger, togglePanelTrigger,
maxPolyphony, maxPolyphony,
multiChannelOrbits,
} = useSettings(); } = useSettings();
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -162,6 +163,22 @@ export function SettingsTab({ started }) {
value={maxPolyphony ?? ''} value={maxPolyphony ?? ''}
/> />
</FormItem> </FormItem>
<FormItem>
<Checkbox
label="Multi Channel Orbits"
onChange={(cbEvent) => {
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}
/>
</FormItem>
<FormItem label="Theme"> <FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} /> <SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem> </FormItem>
@@ -14,6 +14,8 @@ export function SoundsTab() {
const sounds = useStore(soundMap); const sounds = useStore(soundMap);
const { soundsFilter } = useSettings(); const { soundsFilter } = useSettings();
const [search, setSearch] = useState(''); 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(() => { const soundEntries = useMemo(() => {
if (!sounds) { if (!sounds) {
@@ -37,6 +39,9 @@ export function SoundsTab() {
if (soundsFilter === 'synths') { if (soundsFilter === 'synths') {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
} }
if (soundsFilter === 'importSounds') {
return [];
}
return filtered; return filtered;
}, [sounds, soundsFilter, search]); }, [sounds, soundsFilter, search]);
@@ -51,7 +56,6 @@ export function SoundsTab() {
ref?.stop(getAudioContext().currentTime + 0.01); ref?.stop(getAudioContext().currentTime + 0.01);
}); });
}); });
return ( return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground"> <div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} /> <Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
@@ -65,9 +69,9 @@ export function SoundsTab() {
drums: 'drum-machines', drums: 'drum-machines',
synths: 'Synths', synths: 'Synths',
user: 'User', user: 'User',
importSounds: 'import-sounds',
}} }}
></ButtonGroup> ></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div> </div>
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2"> <div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
@@ -101,7 +105,55 @@ export function SoundsTab() {
</span> </span>
); );
})} })}
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''} {!soundEntries.length && soundsFilter === 'importSounds' ? (
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
<p>
To import sounds into strudel, they must be contained{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
within a folder or subfolder
</a>
. The best way to do this is to upload a samples folder containing subfolders of individual sounds or
soundbanks (see diagram below).{' '}
</p>
<pre className="bg-background" key={'sample-diagram'}>
{`└─ samples <-- import this folder
swoop
swoopshort.wav
swooplong.wav
swooptight.wav
smash
smashhigh.wav
smashlow.wav
smashmiddle.wav`}
</pre>
<p>
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!
</p>
<p>
For more information, and other ways to use your own sounds in strudel,{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
check out the docs
</a>
!
</p>
<h3>Preview Sounds</h3>
<pre className="bg-background" key={'sample-preview'}>
n("0 1 2 3 4 5").s("sample-name")
</pre>
<p>
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.
</p>
</div>
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds'
? 'No custom sounds loaded in this pattern (yet).'
: ''}
</div> </div>
</div> </div>
); );
+11 -13
View File
@@ -54,25 +54,23 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob; 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) => { return blobToDataUrl(blob).then((soundPath) => {
const soundPaths = sounds.get(parentDirectory) ?? new Set(); const titlePathMap = sounds.get(parentDirectory) ?? new Map();
soundPaths.add(soundPath);
sounds.set(parentDirectory, soundPaths); titlePathMap.set(title, soundPath);
sounds.set(parentDirectory, titlePathMap);
return; return;
}); });
}), }),
) )
.then(() => { .then(() => {
sounds.forEach((soundPaths, key) => { sounds.forEach((titlePathMap, key) => {
const value = Array.from(soundPaths); 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), { registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample', type: 'sample',
+7 -3
View File
@@ -18,7 +18,7 @@ import { setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { clearHydra } from '@strudel/hydra'; import { clearHydra } from '@strudel/hydra';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../settings.mjs'; import { parseBoolean, settingsMap, useSettings } from '../settings.mjs';
import { import {
setActivePattern, setActivePattern,
setLatestCode, setLatestCode,
@@ -36,11 +36,15 @@ import './Repl.css';
import { setInterval, clearInterval } from 'worker-timers'; import { setInterval, clearInterval } from 'worker-timers';
import { getMetadata } from '../metadata_parser'; import { getMetadata } from '../metadata_parser';
const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get(); const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, audioReady; let modulesLoading, presets, drawContext, clearCanvas, audioReady;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName }); audioReady = initAudioOnFirstClick({
maxPolyphony,
audioDeviceName,
multiChannelOrbits: parseBoolean(multiChannelOrbits),
});
modulesLoading = loadModules(); modulesLoading = loadModules();
presets = prebake(); presets = prebake();
drawContext = getDrawContext(); drawContext = getDrawContext();
+3 -1
View File
@@ -38,6 +38,7 @@ export const defaultSettings = {
isButtonRowHidden: false, isButtonRowHidden: false,
isCSSAnimationDisabled: false, isCSSAnimationDisabled: false,
maxPolyphony: 128, maxPolyphony: 128,
multiChannelOrbits: false,
}; };
let search = null; let search = null;
@@ -50,7 +51,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
export const settingsMap = persistentMap(settings_key, defaultSettings); 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() { export function useSettings() {
const state = useStore(settingsMap); const state = useStore(settingsMap);
@@ -81,6 +82,7 @@ export function useSettings() {
isPanelPinned: parseBoolean(state.isPanelPinned), isPanelPinned: parseBoolean(state.isPanelPinned),
isPanelOpen: parseBoolean(state.isPanelOpen), isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns, userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
}; };
} }