Compare commits

..

68 Commits

Author SHA1 Message Date
Felix Roos 301fb5e59a Merge remote-tracking branch 'origin/main' into example-fixes 2025-11-12 21:35:03 +01:00
Felix Roos 9ac691c19e fix: naming in draw package readme 2025-11-12 21:26:00 +01:00
Felix Roos 6e7512016c chore: package-lock 2025-11-12 21:25:47 +01:00
Felix Roos 3bbd808783 fix: use published web package for headless-repl 2025-11-12 21:25:40 +01:00
Felix Roos 7b245abd1a fix: headless-repl stop button 2025-11-12 21:25:18 +01:00
Felix Roos 9e5c9c7539 fix: update web example 2025-11-12 21:25:08 +01:00
Felix Roos d14194db6a fix: codemirror-repl example 2025-11-12 21:24:56 +01:00
froos 0960741156 Merge pull request '[supradough] fix: unstable filter' (#1593) from supradough-filter-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1593
2025-11-12 21:22:33 +01:00
Alex McLean bbb977afcc Merge pull request 'Fix link syntax in project-start' (#1724) from Kissaki/strudel:kissaki-patch-1 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1724
2025-11-12 21:12:06 +01:00
Alex McLean 81a4981a68 Merge pull request 'Fix web README sample code' (#1725) from Kissaki/strudel:kissaki-patch-2 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1725
2025-11-12 21:06:12 +01:00
Alex McLean 90d82c8988 Merge pull request 'Fix typo: 'studel' -> 'strudel'' (#1726) from drhayes/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1726
2025-11-12 20:59:39 +01:00
froos 15f7b7c1a2 Merge pull request 'fix for node 24 support in tests - #1718' (#1719) from ausav/strudel:ausav.nodever into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1719
2025-11-12 15:23:36 +01:00
froos b6e382508f Merge pull request 'soundAlias example fix' (#1720) from PepsiiMan/strudel:docfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1720
2025-11-11 09:00:47 +01:00
drhayes 3e7ae88043 Fix typo: 'studel' -> 'strudel' 2025-11-10 17:02:55 +01:00
froos 897a2016d0 Merge pull request 'fix: Make Supradough package build' (#1711) from munshkr/strudel:fix/1702-supradough-build into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1711
2025-11-10 08:18:35 +01:00
froos 7a1c1a75b1 Merge pull request 'added docs for spaces in scale names' (#1715) from ondras/strudel:scale-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1715
2025-11-10 08:18:00 +01:00
ondras 7f06f11e96 Merge branch 'main' into scale-docs 2025-11-10 08:08:04 +01:00
Switch Angel AKA Jade Rose 5acb00795e Merge pull request 'Feature: LFOs for Filters' (#1636) from glossing/strudel:glossing/filter-lfo into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1636
2025-11-10 02:44:00 +01:00
Switch Angel AKA Jade Rose f4b68fa2e6 Merge branch 'main' into glossing/filter-lfo 2025-11-10 02:20:20 +01:00
PepsiiMan 1b37f1ed1d soundAlias example fix 2025-11-06 15:33:06 +01:00
ondras f8943dbc94 Merge branch 'main' into scale-docs 2025-11-06 08:16:34 +01:00
Ondřej Žára a3f82f90a9 test fix 2025-11-06 08:15:55 +01:00
Switch Angel AKA Jade Rose 670f75b7e8 Merge pull request 'improvement: sync midi messages to audio context clock' (#1708) from sync_midi_timing into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1708
2025-11-05 17:06:59 +01:00
Jade (Rose) Rowland 79875ebc50 fix branch 2025-11-05 01:26:36 -05:00
Switch Angel AKA Jade Rose a1bd905c06 Merge branch 'main' into sync_midi_timing 2025-11-05 07:25:06 +01:00
Alex Victoria 247bf9b930 fix for node 24 support #1718 2025-11-04 20:54:21 -06:00
froos 8f24ce223f Merge pull request 'Add stick button mappings to gamepad implementation and improve docs' (#1696) from kaosuryoko/strudel:gamepad-stick-button into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1696
2025-11-04 19:36:13 +01:00
froos 51648d3e6e Merge pull request 'Refactor sound stopping and triggering logic in SoundsTab component' (#1688) from IJOL/strudel:bugfix/multi-sample-loading into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1688
2025-11-04 19:29:34 +01:00
froos d4e67e1c20 Merge pull request 'Add setting to toggle pattern auto-start on pattern change' (#1690) from moumar/strudel:pattern-auto-start-setting into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1690
2025-11-04 19:13:20 +01:00
froos edf4739cc6 Merge pull request 'Voicings JSDoc' (#1686) from sharkeys_lunchbox/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1686
2025-11-04 18:58:50 +01:00
froos 3e636bd6f0 Merge branch 'main' into main 2025-11-04 18:55:34 +01:00
Ondřej Žára 4df2cc3086 added docs for spaces in scale names 2025-11-03 22:03:39 +01:00
moumar 809eb7132c codeformat 2025-11-02 15:20:31 +01:00
froos dd6429007e Merge branch 'main' into bugfix/multi-sample-loading 2025-11-02 14:56:30 +01:00
froos e211d5a6bb Merge branch 'main' into pattern-auto-start-setting 2025-11-02 14:44:31 +01:00
Damián Silvani 404d9999f3 fix: Update github.com links 2025-11-02 09:21:40 -03:00
Damián Silvani bc1c83ed64 fix: Add missing vite.config.js to supradough package 2025-11-02 09:21:25 -03:00
Jade (Rose) Rowland 91e1cc136e working 2025-11-01 23:34:10 -04:00
Switch Angel AKA Jade Rose 92e3a80e65 Merge pull request 'FIX: Loading local samples uses too much memory' (#1706) from jade/fix_sample_loading into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1706
2025-11-01 07:50:39 +01:00
Jade (Rose) Rowland 4f046ae883 cf 2025-11-01 02:40:40 -04:00
Jade (Rose) Rowland 2b575d50b9 temporarily disable sample autocomplete 2025-11-01 02:39:43 -04:00
Jade (Rose) Rowland 7b92874909 fix broken array 2025-11-01 01:55:05 -04:00
Jade (Rose) Rowland 10bfe32a2d rm console 2025-11-01 01:48:42 -04:00
Jade (Rose) Rowland a41b07c67a fixed build 2025-11-01 01:44:32 -04:00
Jade (Rose) Rowland 20027d7208 Merge branch 'jade/fix_sample_loading' of ssh://codeberg.org/uzu/strudel into jade/fix_sample_loading 2025-11-01 01:32:35 -04:00
Jade (Rose) Rowland f38792c569 format 2025-11-01 01:32:24 -04:00
Switch Angel AKA Jade Rose 2c2bfd7070 Merge branch 'main' into jade/fix_sample_loading 2025-11-01 06:31:25 +01:00
Jade (Rose) Rowland 484c5e6cdd working 2025-11-01 01:26:25 -04:00
Jason Dufair a036c4f921 Merge remote-tracking branch 'upstream/main' 2025-10-30 18:57:36 -04:00
Jason Dufair c0e7971be7 Merge remote-tracking branch 'upstream/main' 2025-10-27 09:26:47 -04:00
kaosuryoko b491c42af5 Merge branch 'main' into gamepad-stick-button 2025-10-27 00:36:50 +01:00
Greg Ervin ee43d85f29 Add stick button mappings to gamepad implementation and improve docs 2025-10-26 13:31:13 -05:00
moumar fcd83cce55 Re-evaluate pattern if activated 2025-10-26 14:07:36 +01:00
moumar fddf2ff2a7 Set pattern auto-start to true by default 2025-10-26 14:06:31 +01:00
Jason Dufair 6d4787ee33 Test snapshot 2025-10-24 09:21:04 -04:00
Jason Dufair 1a362bd8ea Fix example 2025-10-24 09:19:02 -04:00
moumar 20bbda30f8 Add setting to toggle pattern auto-start on pattern change 2025-10-23 23:15:21 +02:00
Ignacio Ortega a8c613b7eb Refactor sound stopping and triggering logic in SoundsTab component 2025-10-23 19:04:29 +02:00
Jason Dufair fd0a910bfb Voicings JSDoc 2025-10-23 11:11:21 -04:00
Aria ac7e9c6978 Typos on envelope values 2025-10-14 11:37:41 -05:00
Aria c13a726129 Merge branch 'main' into glossing/filter-lfo 2025-10-14 11:24:49 -05:00
Switch Angel AKA Jade Rose 99b3c04a76 Merge branch 'main' into glossing/filter-lfo 2025-10-13 07:04:51 +02:00
Aria 57fac57677 Remove todo 2025-10-09 22:23:28 -05:00
Aria a3b803c3f3 First pass 2025-10-09 18:28:59 -05:00
Kissaki 4d39f36868 Fix web README sample code
Looks like the code was copied, and then the wrong string was replaced when creating this code sample.
2025-09-21 18:31:59 +02:00
Kissaki c64aa3bf01 Fix link syntax in project-start 2025-09-21 18:27:05 +02:00
Felix Roos a0c2edad30 fix: add clamp function 2025-09-18 10:54:41 +02:00
Felix Roos 82c9047195 fix: unstable filter 2025-09-18 10:47:23 +02:00
72 changed files with 7305 additions and 7550 deletions
+1
View File
@@ -0,0 +1 @@
22
+1 -1
View File
@@ -13,7 +13,7 @@ https://strudel.cc/
After cloning the project, you can run the REPL locally:
1. Install [Node.js](https://nodejs.org/)
1. Install [Node.js](https://nodejs.org/) 18 or newer
2. Install [pnpm](https://pnpm.io/installation)
3. Install dependencies by running the following command:
```bash
+1 -1
View File
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<script src="https://unpkg.com/@strudel/web@1.2.6"></script>
<button id="play">PLAY</button>
<script>
initStrudel({
+11 -8
View File
@@ -12,13 +12,16 @@
"vite": "^6.0.11"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
"@strudel/codemirror": "1.2.6",
"@strudel/core": "1.2.5",
"@strudel/draw": "1.2.5",
"@strudel/mini": "1.2.5",
"@strudel/soundfonts": "1.2.6",
"@strudel/tonal": "1.2.5",
"@strudel/transpiler": "1.2.5",
"@strudel/webaudio": "1.2.6"
},
"engines": {
"node": ">=18.0.0"
}
}
+5 -8
View File
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
@@ -28,13 +28,12 @@ samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-dru
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
).fast(4/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
@@ -61,16 +60,14 @@ stack(
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
.late("[0 .05]*2").late(12).fast(2)
`;
export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
+1 -1
View File
@@ -13,7 +13,7 @@
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from '@strudel/web';
import { initStrudel, hush } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
+4 -1
View File
@@ -13,6 +13,9 @@
"vite": "^6.0.11"
},
"dependencies": {
"@strudel/web": "workspace:*"
"@strudel/web": "1.2.6"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -18,5 +18,8 @@
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/tonal": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -13,5 +13,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -32,5 +32,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -73,5 +73,8 @@
"prettier": "^3.4.2",
"vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
}
+14 -10
View File
@@ -1,8 +1,10 @@
import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
import { Scale } from '@tonaljs/tonal';
import { soundMap } from 'superdough';
//TODO: fix tonal scale import
// import { Scale } from '@tonaljs/tonal';
// import { soundMap } from '@strudel/webaudio';
let soundMap = undefined;
import { complex } from '@strudel/tonal';
const escapeHtml = (str) => {
@@ -79,7 +81,9 @@ const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export function bankCompletions() {
const soundDict = soundMap.get();
// TODO: FIX IMPORT
const soundDict = soundMap?.get() ?? {};
const banks = new Set();
for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_');
@@ -90,13 +94,13 @@ export function bankCompletions() {
.map((name) => ({ label: name, type: 'bank' }));
}
// Attempt to get all scale names from Tonal
// Attempt to get all scale names from Tonal TODO: FIX IMPORT
let scaleCompletions = [];
try {
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
} catch (e) {
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
}
// try {
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
// } catch (e) {
// console.warn('[autocomplete] Could not load scale names from Tonal:', e);
// }
// Valid mode values for voicing
const modeCompletions = [
@@ -268,7 +272,7 @@ function soundHandler(context) {
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap.get()).sort();
const soundNames = Object.keys(soundMap?.get() ?? {}).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length;
+3
View File
@@ -54,5 +54,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+198 -6
View File
@@ -1288,6 +1288,151 @@ export const { fanchor } = registerControl('fanchor');
*/
// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496
// ['hpf'],
/**
* Rate of the LFO for the lowpass filter
*
* @name lprate
* @param {number | Pattern} rate rate in hertz
*/
export const { lprate } = registerControl('lprate');
/**
* Cycle-synced rate of the LFO for the lowpass filter
*
* @name lpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { lpsync } = registerControl('lpsync');
/**
* Depth of the LFO for the lowpass filter
*
* @name lpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { lpdepth } = registerControl('lpdepth');
/**
* Shape of the LFO for the lowpass filter
*
* @name lpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { lpshape } = registerControl('lpshape');
/**
* DC offset of the LFO for the lowpass filter
*
* @name lpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { lpdc } = registerControl('lpdc');
/**
* Skew of the LFO for the lowpass filter
*
* @name lpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { lpskew } = registerControl('lpskew');
/**
* Rate of the LFO for the bandpass filter
*
* @name bprate
* @param {number | Pattern} rate rate in hertz
*/
export const { bprate } = registerControl('bprate');
/**
* Cycle-synced rate of the LFO for the bandpass filter
*
* @name bpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { bpsync } = registerControl('bpsync');
/**
* Depth of the LFO for the bandpass filter
*
* @name bpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { bpdepth } = registerControl('bpdepth');
/**
* Shape of the LFO for the bandpass filter
*
* @name bpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { bpshape } = registerControl('bpshape');
/**
* DC offset of the LFO for the bandpass filter
*
* @name bpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { bpdc } = registerControl('bpdc');
/**
* Skew of the LFO for the bandpass filter
*
* @name bpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { bpskew } = registerControl('bpskew');
/**
* Rate of the LFO for the highpass filter
*
* @name hprate
* @param {number | Pattern} rate rate in hertz
*/
export const { hprate } = registerControl('hprate');
/**
* Cycle-synced rate of the LFO for the highpass filter
*
* @name hpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { hpsync } = registerControl('hpsync');
/**
* Depth of the LFO for the highpass filter
*
* @name hpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { hpdepth } = registerControl('hpdepth');
/**
* Shape of the LFO for the highpass filter
*
* @name hpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { hpshape } = registerControl('hpshape');
/**
* DC offset of the LFO for the highpass filter
*
* @name hpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { hpdc } = registerControl('hpdc');
/**
* Skew of the LFO for the highpass filter
*
* @name hpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { hpskew } = registerControl('hpskew');
/**
* Applies a vibrato to the frequency of the oscillator.
*
@@ -1750,17 +1895,64 @@ export const { semitone } = registerControl('semitone');
// TODO: synth param
export const { voice } = registerControl('voice');
// voicings // https://codeberg.org/uzu/strudel/issues/506
// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
/**
* The chord to voice
* @name chord
* @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings
* @example
* chord("<Am C D F Am E Am E>").voicing()
**/
export const { chord } = registerControl('chord');
// which dictionary to use for the voicings
/**
* Which dictionary to use for the voicings. This falls back to the default dictionary if not provided
*
* @name dictionary
* @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use
* @example
* addVoicings('house', {
'': ['7 12 16', '0 7 16', '4 7 12'],
'm': ['0 3 7']
})
chord("<Am C D F Am E Am E>")
.dict('house').anchor(66)
.voicing().room(.5)
**/
export const { dictionary, dict } = registerControl('dictionary', 'dict');
// the top note to align the voicing to, defaults to c5
/** The top note to align the voicing to. Defaults to c5
*
* @name anchor
* @param {string | Pattern} anchorNote the note to align the voicings to
* @example
* anchor("<c4 g4 c5 g5>").chord("C").voicing()
**/
export const { anchor } = registerControl('anchor');
// how the voicing is offset from the anchored position
/**
* Sets how the voicing is offset from the anchored position
*
* @name offset
* @param {number | Pattern} shift the amount to shift the voicing up or down
* @example
* chord("<Am C D F Am E Am E>").offset("<0 1 2 3 4 5>") // alter the voicing each time
**/
export const { offset } = registerControl('offset');
// how many octaves are voicing steps spread apart, defaults to 1
/**
* How many octaves are voicing steps spread apart, defaults to 1
*
* @name octaves
* @param {number | Pattern} count the number of octaves
* @example
* chord("<Am C D F Am E Am E>").octaves("<2 4>").voicing()
**/
export const { octaves } = registerControl('octaves');
// below = anchor note will be removed from the voicing, useful for melody harmonization
/**
* Remove anchor note from the voicing. Useful for melody harmonization
*
* @name mode
* @param {string | Pattern} modeName one of {below | above | duck | root}
* @example
* mode("<below above duck root>").chord("C").voicing()
*
**/
export const { mode } = registerControl(['mode', 'anchor']);
/**
+3
View File
@@ -37,5 +37,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -38,5 +38,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -25,5 +25,8 @@
"@strudel/core": "workspace:*",
"@tauri-apps/api": "^2.2.0"
},
"homepage": "https://codeberg.org/uzu/strudel#readme"
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel/canvas
# @strudel/draw
Helpers for drawing with the Canvas API and Strudel
## Install
```sh
npm i @strudel/canvas --save
npm i @strudel/draw --save
```
+3
View File
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+4 -1
View File
@@ -20,5 +20,8 @@
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme"
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
}
+7 -33
View File
@@ -14,12 +14,7 @@ npm i @strudel/gamepad --save
import { gamepad } from '@strudel/gamepad';
// Initialize gamepad (optional index parameter, defaults to 0)
const pad = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
//const pad = gamepad(0, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
const pad = gamepad(0);
// Use gamepad inputs in patterns
const pattern = sequence([
@@ -45,6 +40,12 @@ const pattern = sequence([
- D-Pad
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
- Stick Buttons
- `l3`, `r3` (or `ls`, `rs`)
- Toggle versions: `tglL3`, `tglR3` (or `tglLS`, `tglRS`)
- System Buttons
- `start`, `back` (or uppercase `START`, `BACK`)
- Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`)
### Analog Sticks
- Left Stick
@@ -91,35 +92,8 @@ $: sound("hadoken").gain(pad.checkSequence(HADOKEN))
## Multiple Gamepads
You can connect multiple gamepads by specifying the gamepad index:
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
```javascript
const pad1 = gamepad(0); // First gamepad
const pad2 = gamepad(1); // Second gamepad
```
## Vibration
You can use the `vibrate` control to provide haptic feedback to the gamepad.
The `vibrate` control is a patternable control, so you can use it to control the vibration of the gamepad.
The vibrate control has several parameters:
- `vibGamepadIndex` - Index of the gamepad to vibrate (0-3)
- `vibStrong` - Intensity of the strong/left motor (0-1)
- `vibWeak` - Intensity of the weak/right motor (0-1)
- `vibDuration` - Duration of vibration in milliseconds
- `vibEnable` - Enable/disable vibration (0 or 1)
```javascript
// Vibration with gamepad
// vibrate(gamepadId, { strong: 1, weak: 0.5, duration: 100 })
// vibrate("gamepadId")
// vibrate(gamepadId)
$: note("c4*4").vibrate(0, { strong: 1, weak: 0.5, duration: 100 }) // gamepad index 0
$: note("c4*4").vibrate("0:1:0.5:200") // Array format
$: note("c4*4").vibrate("<0 1 0 1>") // select gamepad index
$: note("c4*4").vibStrong("<0.1 0.2 0.5 1>").vibWeak("<0.1 0.2 0.5 1>").vibDuration("<10 20 50 100>").vibrate(0) // gamepad index 0
```
+5 -24
View File
@@ -29,6 +29,10 @@ The gamepad module provides access to buttons and analog sticks as normalized si
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
| Stick Buttons | `l3`, 'r3' (or `ls`, `rs`) |
| | Toggle versions: `tglL3`, 'tglR3' (or `tglLs`, `tglRs`) |
| System Buttons | `start`, `back` (or uppercase `START`, `BACK`) |
| | Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`) |
### Analog Sticks
@@ -107,32 +111,9 @@ $: s("free_hadouken -").slow(2)
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
`} />
### Button Sequences
### Button Mappings
The gamepad module supports different button mapping configurations to accommodate various gamepad layouts:
- **XBOX** (Default): Standard Xbox-style mapping where A=0, B=1, X=2, Y=3
- **NES**: Nintendo-style mapping where B=0, A=1, Y=2, X=3
- **Custom**: Define your own button mapping by passing an object with button assignments
You can specify the mapping when initializing the gamepad:
<MiniRepl
client:idle
tune={`
const pad1 = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
const pad2 = gamepad(1, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
const pad3 = gamepad(2, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
const pad4 = gamepad(3, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
`}
/>
### Multiple Gamepads
## Multiple Gamepads
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
<MiniRepl
client:idle
+36 -147
View File
@@ -1,77 +1,39 @@
// @strudel/gamepad/index.mjs
import { signal, Pattern, logger, registerControl, register, isPattern } from '@strudel/core';
import { vibrationSupported, getGamepadVibrationActuator } from './vibration.mjs';
import { signal } from '@strudel/core';
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
export const buttonMapSettings = {
XBOX: {
// XBOX mapping default
a: 0,
b: 1,
x: 2,
y: 3,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
lstick: 10,
rstick: 11,
ls: 10,
rs: 11,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
xbox: 16,
},
NES: {
// Nintendo mapping
a: 1,
b: 0,
x: 3,
y: 2,
lb: 4,
rb: 5,
zl: 6,
zr: 7,
lt: 6,
rt: 7,
back: 8,
minus: 8,
start: 9,
plus: 9,
lstick: 10,
rstick: 11,
ls: 10,
rs: 11,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
home: 16,
select: 17,
},
export const buttonMap = {
a: 0,
b: 1,
x: 2,
y: 3,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
l3: 10,
ls: 10,
r3: 11,
rs: 11,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
};
class ButtonSequenceDetector {
constructor(timeWindow = 1000, mapping) {
constructor(timeWindow = 1000) {
this.sequence = [];
this.timeWindow = timeWindow;
this.lastInputTime = 0;
this.buttonStates = Array(Object.keys(mapping).length).fill(0); // Track previous state of each button
this.buttonMap = mapping;
this.buttonStates = Array(16).fill(0); // Track previous state of each button
// Button mapping for character inputs
}
@@ -86,8 +48,7 @@ class ButtonSequenceDetector {
}
// Store the button name instead of index
const buttonName =
Object.keys(this.buttonMap).find((key) => this.buttonMap[key] === buttonIndex) || buttonIndex.toString();
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
this.sequence.push({
input: buttonName,
@@ -130,9 +91,9 @@ class ButtonSequenceDetector {
// Check if either the input matches directly or they refer to the same button in the map
return (
input === target ||
this.buttonMap[input] === this.buttonMap[target] ||
buttonMap[input] === buttonMap[target] ||
// Also check if the numerical index matches
this.buttonMap[input] === parseInt(target)
buttonMap[input] === parseInt(target)
);
})
? 1
@@ -141,13 +102,12 @@ class ButtonSequenceDetector {
}
class GamepadHandler {
constructor(index = 0, mapping) {
constructor(index = 0) {
// Add index parameter
this._gamepads = {};
this._mapping = mapping;
this._activeGamepad = index; // Use provided index
this._axes = [0, 0, 0, 0];
this._buttons = Array(Object.keys(mapping).length).fill(0);
this._buttons = Array(16).fill(0);
this.setupEventListeners();
}
@@ -187,66 +147,12 @@ class GamepadHandler {
}
}
// Add utility function to list all connected gamepads
export const listGamepads = () => {
const gamepads = navigator.getGamepads();
const connectedGamepads = Array.from(gamepads)
.filter((gp) => gp !== null)
.map((gp) => ({
index: gp.index,
id: gp.id,
mapping: gp.mapping,
buttons: gp.buttons.length,
axes: gp.axes.length,
connected: gp.connected,
timestamp: gp.timestamp,
}));
// Format the gamepads info into a readable string
const gamepadsInfo = connectedGamepads.map((gp) => `${gp.index}: ${gp.id}`).join('\n');
logger(`[gamepad] available gamepads:\n${gamepadsInfo}`);
return connectedGamepads;
};
// Module-level state store for toggle states
const gamepadStates = new Map();
export const gamepad = (index = 0, mapping = 'XBOX') => {
// list connected gamepads
const connectedGamepads = listGamepads();
// Check if the requested gamepad index exists
const requestedGamepad = connectedGamepads.find((gp) => gp.index === index);
if (!requestedGamepad) {
throw new Error(
`[gamepad] gamepad at index ${index} not found. available gamepads: ${connectedGamepads.map((gp) => gp.index).join(', ')}`,
);
}
// Handle button mapping
let buttonMap = buttonMapSettings.XBOX;
if (typeof mapping === 'string') {
buttonMap = buttonMapSettings[mapping.toUpperCase()];
} else if (typeof mapping === 'object') {
buttonMap = { ...buttonMapSettings.XBOX, ...mapping };
// Check that all mapping values are valid button indices
const maxButtons = requestedGamepad.buttons;
for (const [key, value] of Object.entries(mapping)) {
if (typeof value !== 'number' || value < 0 || value >= maxButtons) {
throw new Error(
`[gamepad] invalid button mapping for '${key}': ${value}. Must be a number between 0 and ${maxButtons - 1}`,
);
}
}
}
if (!buttonMap) {
throw new Error(`[gamepad] button mapping '${mapping}' not found`);
}
const handler = new GamepadHandler(index, buttonMap);
const sequenceDetector = new ButtonSequenceDetector(2000, buttonMap);
export const gamepad = (index = 0) => {
const handler = new GamepadHandler(index);
const sequenceDetector = new ButtonSequenceDetector(2000);
// Base signal that polls gamepad state and handles sequence detection
const baseSignal = signal((t) => {
@@ -277,7 +183,7 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
axes.y2_2 = axes.y2.toBipolar();
// Create button patterns
const buttons = Array(Object.keys(buttonMap).length)
const buttons = Array(16)
.fill(null)
.map((_, i) => {
// Create unique key for this gamepad+button combination
@@ -316,22 +222,8 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
};
const checkSequence = btnSequence;
const sequence = btnSequence;
const btnSeq = btnSequence;
const btnseq = btnSequence;
const seq = btnSequence;
// Create vibration pattern
const vibration = (pattern) => {
return signal(() => {
// Return vibration enable/disable pattern
return pattern;
});
};
logger(
`[gamepad] connected to gamepad ${index} (${requestedGamepad.id}) with ${typeof mapping === 'object' ? 'custom' : mapping} mapping${vibrationSupported(index) ? ' (vibration supported)' : ''}`,
);
const btnseq = btnSeq;
// Return an object with all controls
return {
@@ -346,12 +238,9 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
]),
),
checkSequence,
sequence,
btnSequence,
btnSeq,
btnseq,
seq,
vibration,
raw: baseSignal,
};
};
+3
View File
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-288
View File
@@ -1,288 +0,0 @@
// Gamepad Vibration Controls
import { registerControl, register, logger, isPattern } from '@strudel/core';
import { Pattern } from '@strudel/core';
/**
* Controls the strong motor intensity for gamepad vibration.
*
* @name vibStrong
* @param {number | Pattern} intensity between 0 and 1
* @synonyms vibS
* @example
* s("bd hh sd hh").vibStrong("1 0 0.5 0").vibrate(0)
*/
export const { vibStrong, vibS } = registerControl('vibStrong', 'vibS');
/**
* Controls the weak motor intensity for gamepad vibration.
*
* @name vibWeak
* @param {number | Pattern} intensity between 0 and 1
* @synonyms vibW
* @example
* s("bd hh sd hh").vibWeak("0.5 0 0.8 0").vibrate(0)
*/
export const { vibWeak, vibW } = registerControl('vibWeak', 'vibW');
/**
* Controls the duration of gamepad vibration in milliseconds.
*
* @name vibDuration
* @param {number | Pattern} duration in milliseconds
* @synonyms vibDur
* @example
* s("bd hh sd hh").vibDuration("200 0 100 0").vibrate(0)
*/
export const { vibDuration, vibDur } = registerControl('vibDuration', 'vibDur');
/**
* Enables or disables gamepad vibration for individual events.
*
* @name vibEnable
* @param {number | Pattern} enable 1 to enable, 0 to disable
* @synonyms vibE
* @example
* s("bd hh sd hh").vibEnable("1 0 1 0").vibrate(0)
*/
export const { vibEnable, vibEn } = registerControl('vibEnable', 'vibEn');
/**
* Sets the gamepad index for vibration events.
*
* @name vibGamepadIndex
* @param {number | Pattern} index gamepad index (0-3)
* @synonyms vibGpId, vibGamepadIndex, vibGamepadId
* @example
* s("bd hh sd hh").vibGamepadIndex("0 1 0 1").vibrate()
*/
export const { vibGamepadIndex, vibGpId, vibGamepadId } = registerControl('vibGamepadIndex', 'vibGpId', 'vibGamepadId');
/**
* Sets both strong and weak vibration intensity at once.
*
* @name vibIntensity
* @param {number | Pattern} intensity between 0 and 1, applied to both motors
* @example
* s("bd*4").vibIntensity(sine.slow(2).range(0, 1)).vibrate(0)
*/
export const vibIntensity = register('vibIntensity', (intensity, pat) => {
return pat.vibStrong(intensity).vibWeak(intensity);
});
export const vibrationSupported = (index) => {
const gamepad = navigator.getGamepads()[index];
return gamepad?.vibrationActuator?.type === 'dual-rumble';
};
export const getGamepadVibrationActuator = (index) => {
const gamepad = navigator.getGamepads()[index];
if (!gamepad) {
throw new Error(`[gamepad] gamepad at index ${index} not found`);
}
if (!gamepad.vibrationActuator) {
throw new Error(`[gamepad] gamepad at index ${index} does not support vibration`);
}
return gamepad.vibrationActuator;
};
// Default vibration latency in milliseconds
const DEFAULT_VIBRATION_LATENCY = 10;
/**
* Parses vibration string format "id:strong:weak:duration"
* @param {string} vibString - The vibration string to parse
* @returns {object} Parsed vibration parameters
*/
function parseVibrationString(vibString) {
if (typeof vibString !== 'string') {
return null;
}
const parts = vibString.split(':');
if (parts.length < 1) {
return null;
}
const [id, strong, weak, duration] = parts;
return {
vibGamepadIndex: parseInt(id) || 0,
vibStrong: parseFloat(strong) || 0.8,
vibWeak: parseFloat(weak) || 0.4,
vibDuration: parseInt(duration) || 100,
vibEnable: 1,
};
}
/**
* Sends vibration commands to a gamepad based on pattern events.
*
* @name vibrate
* @memberof Pattern
* @param {number | string | Pattern} gamepadIndex - Index of the gamepad to vibrate (0-3), can be patterned, or vibration string
* @param {object} options - Default vibration options
* @param {number} options.strong - Default strong motor intensity (0-1)
* @param {number} options.weak - Default weak motor intensity (0-1)
* @param {number} options.duration - Default duration in milliseconds
* @param {number} options.latency - Latency compensation in milliseconds
* @returns {Pattern}
* @example
* // Basic vibration on beat
* s("bd hh sd hh").vibrate(0)
*
* @example
* // Using hap values for vibration control
* s("<bd hh sd hh>").vibStrong("<1 0.1 0.5 0>").vibWeak("<0.5 0 0.8 0>").vibrate(0)
*
* @example
* // With default options
* s("bd*4").vibrate(0, { strong: 1, weak: 0.5, duration: 100 })
*
* @example
* // Using gamepad input to control vibration
* let gp = gamepad(0);
* note("c a f e").gain(gp.a).vibrate(0, { strong: gp.x1, duration: 200 })
*
* @example
* // Patternable gamepad index - alternate between gamepads
* s("bd hh sd hh").vibrate("<0 1 0 1>")
*
* @example
* // Complex gamepad switching pattern
* s("bd*8").vibrate("0 1 0 [1 0] 1 0 1 0")
*
* @example
* // Using control parameter for gamepad index
* s("bd hh sd hh").vibGamepad("<0 1 0 1>").vibrate()
*
* @example
* // Assignable vibration syntax: "id:strong:weak:duration"
* s("bd hh sd hh").vibrate("0:1:0.5:200 1:0.8:0.3:150")
*
* @example
* // Complex assignable patterns
* s("bd*8").vibrate("0:0.2:0.4:100 0:0.2:1:100 1:1:0.5:200 1:0.5:1:150")
*/
export const { vibrate } = registerControl(['vibGamepad', 'vibStrong', 'vibWeak', 'vibDuration']);
Pattern.prototype.vibrate = function (gamepadIndex, options = {}) {
// Default options
const defaultOptions = {
strong: 0.8,
weak: 0.4,
duration: 100,
latency: DEFAULT_VIBRATION_LATENCY,
...options,
};
// If gamepadIndex is a pattern, handle it based on its content
if (isPattern(gamepadIndex)) {
return this.set(
gamepadIndex.fmap((value) => {
// Simple gamepad index pattern
return { vibGamepadIndex: value };
}),
).onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions);
}, false);
}
// If gamepadIndex is provided as static value, use it directly
// If undefined, will be handled in vibrateHandler (using vibGamepad control or default 0)
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions, gamepadIndex);
}, false);
};
// Helper function to handle the actual vibration logic
function vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions, staticGamepadIndex = null) {
try {
// Determine gamepad index - from hap value or static parameter
let gamepadIndex;
if (staticGamepadIndex !== null && staticGamepadIndex !== undefined) {
gamepadIndex = staticGamepadIndex;
} else if (typeof hap.value === 'object' && ('vibGamepadIndex' in hap.value || 'vibGamepad' in hap.value)) {
const vibGamepadValue = hap.value.vibGamepadIndex;
gamepadIndex = Array.isArray(vibGamepadValue) ? vibGamepadValue[0] : vibGamepadValue;
} else {
gamepadIndex = 0; // fallback default
}
// Validate gamepad index
gamepadIndex = Math.max(0, Math.min(3, Math.floor(Number(gamepadIndex) || 0)));
// Check if vibration is supported
if (!vibrationSupported(gamepadIndex)) {
logger(`[gamepad] vibration not supported on gamepad ${gamepadIndex}`, 'warning');
return;
}
const vibrationActuator = getGamepadVibrationActuator(gamepadIndex);
// Extract vibration parameters from hap value or use defaults
let vibStrong, vibWeak, vibDuration, vibEnable;
if (typeof hap.value === 'object') {
const vibGamepadValue = hap.value.vibGamepadIndex;
// Handle array format: [index, strong, weak, duration]
if (Array.isArray(vibGamepadValue)) {
vibStrong = vibGamepadValue[1] !== undefined ? vibGamepadValue[1] : defaultOptions.strong;
vibWeak = vibGamepadValue[2] !== undefined ? vibGamepadValue[2] : defaultOptions.weak;
vibDuration = vibGamepadValue[3] !== undefined ? vibGamepadValue[3] : defaultOptions.duration;
vibEnable = 1;
} else {
// Regular object destructuring for non-array values
({
vibStrong = defaultOptions.strong,
vibWeak = defaultOptions.weak,
vibDuration = defaultOptions.duration,
vibEnable = 1,
} = hap.value);
}
} else {
// If hap.value is a primitive, use it as vibEnable (0 = off, 1 = on)
vibEnable = hap.value;
vibStrong = defaultOptions.strong;
vibWeak = defaultOptions.weak;
vibDuration = defaultOptions.duration;
}
// Skip vibration if disabled
if (!vibEnable || vibEnable === 0) {
return;
}
// Validate parameters
vibStrong = Math.max(0, Math.min(1, Number(vibStrong) || 0));
vibWeak = Math.max(0, Math.min(1, Number(vibWeak) || 0));
vibDuration = Math.max(0, Number(vibDuration) || defaultOptions.duration);
// Calculate timing offset
const offset = (targetTime - currentTime + defaultOptions.latency / 1000) * 1000;
// Schedule vibration
window.setTimeout(
() => {
try {
vibrationActuator
.playEffect('dual-rumble', {
duration: vibDuration,
strongMagnitude: vibStrong,
weakMagnitude: vibWeak,
})
.catch((err) => {
logger(`[gamepad] vibration failed: ${err.message}`, 'warning');
});
} catch (err) {
logger(`[gamepad] vibration error: ${err.message}`, 'warning');
}
},
Math.max(0, offset),
);
} catch (err) {
logger(`[gamepad] vibration setup failed: ${err.message}`, 'warning');
}
}
+3
View File
@@ -34,5 +34,8 @@
"devDependencies": {
"tree-sitter-haskell": "^0.23.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -40,5 +40,8 @@
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+59 -42
View File
@@ -5,9 +5,10 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
import { scheduleAtTime } from '../superdough/helpers.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -190,7 +191,7 @@ function mapCC(mapping, value) {
}
// sends a cc message to the given device on the given channel
function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
function sendCC(ccn, ccv, device, midichan, targetTime) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -198,19 +199,23 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendControlChange(ccn, scaled, midichan);
}, targetTime);
}
// sends a program change message to the given device on the given channel
function sendProgramChange(progNum, device, midichan, timeOffsetString) {
function sendProgramChange(progNum, device, midichan, targetTime) {
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
throw new Error('expected progNum (program change) to be a number between 0 and 127');
}
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendProgramChange(progNum, midichan);
}, targetTime);
}
// sends a sysex message to the given device on the given channel
function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
function sendSysex(sysexid, sysexdata, device, targetTime) {
if (Array.isArray(sysexid)) {
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysexid bytes must be integers between 0 and 255');
@@ -225,11 +230,13 @@ function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysex bytes must be integers between 0 and 255');
}
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendSysex(sysexid, sysexdata);
}, targetTime);
}
// sends a NRPN message to the given device on the given channel
function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
if (Array.isArray(nrpnn)) {
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all nrpnn bytes must be integers between 0 and 255');
@@ -237,28 +244,34 @@ function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
}
device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendNRPN(nrpnn, nrpv, midichan);
}, targetTime);
}
// sends a pitch bend message to the given device on the given channel
function sendPitchBend(midibend, device, midichan, timeOffsetString) {
function sendPitchBend(midibend, device, midichan, targetTime) {
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
throw new Error('expected midibend to be a number between -1 and 1');
}
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendPitchBend(midibend, midichan);
}, targetTime);
}
// sends a channel aftertouch message to the given device on the given channel
function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
function sendAftertouch(miditouch, device, midichan, targetTime) {
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
throw new Error('expected miditouch to be a number between 0 and 1');
}
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendChannelAftertouch(miditouch, midichan);
}, targetTime);
}
// sends a note message to the given device on the given channel
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
function sendNote(note, velocity, duration, device, midichan, targetTime) {
if (note == null || note === '') {
throw new Error('note cannot be null or empty');
}
@@ -268,12 +281,12 @@ function sendNote(note, velocity, duration, device, midichan, timeOffsetString)
if (duration != null && (typeof duration !== 'number' || duration < 0)) {
throw new Error('duration must be a positive number');
}
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
time: timeOffsetString,
});
scheduleAtTime(() => {
device.playNote(midiNote, midichan);
}, targetTime);
}
/**
@@ -309,7 +322,6 @@ Pattern.prototype.midi = function (midiport, options = {}) {
let midiConfig = {
// Default configuration values
isController: false, // Disable sending notes for midi controllers
latencyMs: 34, // Default latency to get audio engine to line up in ms
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
midichannel: 1, // Default MIDI channel
velocity: 0.9, // Default velocity
@@ -333,18 +345,13 @@ Pattern.prototype.midi = function (midiport, options = {}) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((hap, currentTime, cps, targetTime) => {
return this.onTrigger((hap, _currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
return;
}
hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = midiConfig.latencyMs;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// midi event values from hap with configurable defaults
let {
note,
@@ -380,7 +387,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if midimap is set, send a cc messages from defined controls
if (midicontrolMap.has(midimap)) {
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, targetTime));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
@@ -392,12 +399,12 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// try to prevent glitching by subtracting noteOffsetMs from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
sendNote(note, velocity, duration, device, midichan, targetTime);
}
// Handle program change
if (progNum !== undefined) {
sendProgramChange(progNum, device, midichan, timeOffsetString);
sendProgramChange(progNum, device, midichan, targetTime);
}
// Handle sysex
@@ -407,53 +414,63 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if sysexid is an array the first byte is 0x00
if (sysexid !== undefined && sysexdata !== undefined) {
sendSysex(sysexid, sysexdata, device, timeOffsetString);
sendSysex(sysexid, sysexdata, device, targetTime);
}
// Handle control change
if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, timeOffsetString);
sendCC(ccn, ccv, device, midichan, targetTime);
}
// Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
sendNRPN(nrpnn, nrpv, device, midichan, targetTime);
}
// Handle midibend
if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, timeOffsetString);
sendPitchBend(midibend, device, midichan, targetTime);
}
// Handle miditouch
if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, timeOffsetString);
sendAftertouch(miditouch, device, midichan, targetTime);
}
// Handle midicmd
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
}
if (['clock', 'midiClock'].includes(midicmd)) {
device.sendClock({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendClock();
}, targetTime);
} else if (['start'].includes(midicmd)) {
device.sendStart({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
} else if (['stop'].includes(midicmd)) {
device.sendStop({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendStop();
}, targetTime);
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
scheduleAtTime(() => {
device.sendContinue();
}, targetTime);
} else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
sendProgramChange(midicmd[1], device, midichan, targetTime);
} else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime);
}
} else if (midicmd[0] === 'sysex') {
if (midicmd.length === 3) {
const [_, id, data] = midicmd;
sendSysex(id, data, device, timeOffsetString);
sendSysex(id, data, device, targetTime);
}
}
}
+3
View File
@@ -35,5 +35,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -38,5 +38,8 @@
"peggy": "^4.2.0",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -33,5 +33,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -40,5 +40,8 @@
"mondo": "*",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -34,5 +34,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -44,5 +44,8 @@
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -33,5 +33,8 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -48,5 +48,8 @@
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -15,5 +15,8 @@
"type": "module",
"dependencies": {
"cowsay": "^1.6.0"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -33,5 +33,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -37,5 +37,8 @@
"devDependencies": {
"node-fetch": "^3.3.2",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+60 -34
View File
@@ -154,6 +154,24 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
export function getParamLfo(audioContext, param, start, end, lfoValues) {
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
let lfo;
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
return lfo;
}
// helper utility for applying standard modulators to a parameter
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
@@ -170,55 +188,58 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
let lfo;
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
let filter;
let frequencyParam;
export function createFilter(context, start, end, params, cps) {
let {
frequency,
anchor,
env,
type,
model,
q = 1,
drive = 0.69,
depth,
dcoffset = -0.5,
skew,
shape,
rate,
sync,
} = params;
let frequencyParam, filter;
if (model === 'ladder') {
filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive });
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
frequencyParam = filter.parameters.get('frequency');
} else {
filter = context.createBiquadFilter();
filter.type = type;
filter.Q.value = Q;
filter.Q.value = q;
filter.frequency.value = frequency;
frequencyParam = filter.frequency;
}
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
const [attack, decay, sustain, release] = getADSRValues(envelopeValues, 'exponential', [0.005, 0.14, 0, 0.1]);
// envelope is active when any of these values is set
const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv;
const hasEnvelope = [...envelopeValues, env].some((v) => v !== undefined);
// Apply ADSR to filter frequency
if (hasEnvelope !== undefined) {
fenv = nanFallback(fenv, 1, true);
fanchor = nanFallback(fanchor, 0, true);
const fenvAbs = Math.abs(fenv);
const offset = fenvAbs * fanchor;
if (hasEnvelope) {
env = nanFallback(env, 1, true);
anchor = nanFallback(anchor, 0, true);
const envAbs = Math.abs(env);
const offset = envAbs * anchor;
let min = clamp(2 ** -offset * frequency, 0, 20000);
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
if (fenv < 0) [min, max] = [max, min];
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve);
return filter;
let max = clamp(2 ** (envAbs - offset) * frequency, 0, 20000);
if (env < 0) [min, max] = [max, min];
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, 'exponential');
}
if (sync != null) {
rate = cps * sync;
}
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
getParamLfo(context, frequencyParam, start, end, lfoValues);
return filter;
}
@@ -280,6 +301,11 @@ export function getVibratoOscillator(param, value, t) {
return vibratoOscillator;
}
}
export function scheduleAtTime(callback, targetTime, audioContext = getAudioContext()) {
const currentTime = audioContext.currentTime;
webAudioTimeout(audioContext, callback, currentTime, targetTime);
}
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
+3
View File
@@ -37,5 +37,8 @@
},
"dependencies": {
"nanostores": "^0.11.3"
},
"engines": {
"node": ">=18.0.0"
}
}
+69 -69
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
@@ -146,11 +146,6 @@ let defaultDefaultValues = {
gain: 0.8,
postgain: 1,
density: '.03',
ftype: '12db',
fanchor: 0,
resonance: 1,
hresonance: 1,
bandq: 1,
channels: [1, 2],
phaserdepth: 0.75,
shapevol: 1,
@@ -415,32 +410,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
djf,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
release = 0,
// low pass
cutoff,
lpenv,
lpattack,
lpdecay,
lpsustain,
lprelease,
resonance = getDefaultValue('resonance'),
// high pass
hpenv,
hcutoff,
hpattack,
hpdecay,
hpsustain,
hprelease,
hresonance = getDefaultValue('hresonance'),
// band pass
bpenv,
bandf,
bpattack,
bpdecay,
bpsustain,
bprelease,
bandq = getDefaultValue('bandq'),
//phaser
phaserrate: phaser,
@@ -565,57 +535,87 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// gain stage
chain.push(gainNode(gain));
//filter
// filter
const ftype = getFilterType(value.ftype);
if (cutoff !== undefined) {
let lp = () =>
createFilter(
ac,
'lowpass',
cutoff,
resonance,
lpattack,
lpdecay,
lpsustain,
lprelease,
lpenv,
t,
end,
fanchor,
ftype,
drive,
);
if (value.cutoff !== undefined) {
const lpMap = {
frequency: 'cutoff',
q: 'resonance',
attack: 'lpattack',
decay: 'lpdecay',
sustain: 'lpsustain',
release: 'lprelease',
env: 'lpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'lprate',
sync: 'lpsync',
depth: 'lpdepth',
shape: 'lpshape',
dcoffset: 'lpdc',
skew: 'lpskew',
};
const lpParams = pickAndRename(value, lpMap);
lpParams.type = 'lowpass';
let lp = () => createFilter(ac, t, end, lpParams, cps);
chain.push(lp());
if (ftype === '24db') {
chain.push(lp());
}
}
if (hcutoff !== undefined) {
let hp = () =>
createFilter(
ac,
'highpass',
hcutoff,
hresonance,
hpattack,
hpdecay,
hpsustain,
hprelease,
hpenv,
t,
end,
fanchor,
);
if (value.hcutoff !== undefined) {
const hpMap = {
frequency: 'hcutoff',
q: 'hresonance',
attack: 'hpattack',
decay: 'hpdecay',
sustain: 'hpsustain',
release: 'hprelease',
env: 'hpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'hprate',
sync: 'hpsync',
depth: 'hpdepth',
shape: 'hpshape',
dcoffset: 'hpdc',
skew: 'hpskew',
};
const hpParams = pickAndRename(value, hpMap);
hpParams.type = 'highpass';
let hp = () => createFilter(ac, t, end, hpParams, cps);
chain.push(hp());
if (ftype === '24db') {
chain.push(hp());
}
}
if (bandf !== undefined) {
let bp = () =>
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
if (value.bandf !== undefined) {
const bpMap = {
frequency: 'bandf',
q: 'bandq',
attack: 'bpattack',
decay: 'bpdecay',
sustain: 'bpsustain',
release: 'bprelease',
env: 'bpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'bprate',
sync: 'bpsync',
depth: 'bpdepth',
shape: 'bpshape',
dcoffset: 'bpdc',
skew: 'bpskew',
};
const bpParams = pickAndRename(value, bpMap);
bpParams.type = 'bandpass';
let bp = () => createFilter(ac, t, end, bpParams, cps);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
+5
View File
@@ -109,3 +109,8 @@ export function getCommonSampleInfo(hapValue, bank) {
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
}
/** Selects entries from `source` and renames them via `map` */
export const pickAndRename = (source, map) => {
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
};
+4 -2
View File
@@ -1,11 +1,12 @@
// this is dough, the superdough without dependencies
// @ts-check
// @ts-nocheck
// @ts-ignore ignore next line because sampleRate is unknown
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
const ISR = 1 / SAMPLE_RATE;
let gainCurveFunc = (val) => Math.pow(val, 2);
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
function applyGainCurve(val) {
return gainCurveFunc(val);
@@ -151,7 +152,8 @@ export class TwoPoleFilter {
resonance = Math.max(resonance, 0);
cutoff = Math.min(cutoff, 20000);
const c = 2 * Math.sin(cutoff * PI_DIV_SR);
let c = 2 * Math.sin(cutoff * PI_DIV_SR);
c = clamp(c, 0, 1.14); // this line prevents instability TODO: test
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c;
+6 -3
View File
@@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@@ -25,12 +25,15 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"engines": {
"node": ">=18.0.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
target: 'esnext',
},
});
+3
View File
@@ -25,5 +25,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -38,5 +38,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+5
View File
@@ -229,6 +229,9 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
*
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
* The scale name must be written without spaces (because it would be interpreted as a multi-step pattern otherwise).
* If your scale name includes spaces, replace them with colons.
*
* The root note defaults to octave 3, if no octave number is given.
*
* @name scale
@@ -250,6 +253,8 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* .s("piano")
* @example
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
* @example
* n("[0 0] [1 2] [3 4] [5 6]").scale("C:major:blues")
*/
export const scale = register(
'scale',
+3
View File
@@ -39,5 +39,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
@@ -12,5 +12,8 @@
"type": "module",
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ initStrudel();
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
document.getElementById('play').addEventListener('stop',
document.getElementById('stop').addEventListener('click',
() => hush()
);
```
+3
View File
@@ -43,5 +43,8 @@
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -40,5 +40,8 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -35,5 +35,8 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
-1
View File
@@ -41,7 +41,6 @@ Tune.prototype.tonicize = function(newTonic) {
this.tonic = newTonic
}
/* Return data in the mode you are in (freq, ratio, or midi) */
Tune.prototype.note = function(input,octave){
+132 -20
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
version: 3.0.4(vitest@3.0.4)
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -85,29 +85,29 @@ importers:
examples/codemirror-repl:
dependencies:
'@strudel/codemirror':
specifier: workspace:*
version: link:../../packages/codemirror
specifier: 1.2.6
version: 1.2.6(@codemirror/lint@6.8.4)
'@strudel/core':
specifier: workspace:*
version: link:../../packages/core
specifier: 1.2.5
version: 1.2.5
'@strudel/draw':
specifier: workspace:*
version: link:../../packages/draw
specifier: 1.2.5
version: 1.2.5
'@strudel/mini':
specifier: workspace:*
version: link:../../packages/mini
specifier: 1.2.5
version: 1.2.5
'@strudel/soundfonts':
specifier: workspace:*
version: link:../../packages/soundfonts
specifier: 1.2.6
version: 1.2.6
'@strudel/tonal':
specifier: workspace:*
version: link:../../packages/tonal
specifier: 1.2.5
version: 1.2.5
'@strudel/transpiler':
specifier: workspace:*
version: link:../../packages/transpiler
specifier: 1.2.5
version: 1.2.5
'@strudel/webaudio':
specifier: workspace:*
version: link:../../packages/webaudio
specifier: 1.2.6
version: 1.2.6
devDependencies:
vite:
specifier: ^6.0.11
@@ -116,8 +116,8 @@ importers:
examples/headless-repl:
dependencies:
'@strudel/web':
specifier: workspace:*
version: link:../../packages/web
specifier: 1.2.6
version: 1.2.6
devDependencies:
vite:
specifier: ^6.0.11
@@ -2544,6 +2544,33 @@ packages:
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@strudel/codemirror@1.2.6':
resolution: {integrity: sha512-zjKbnbnOY40dlIAQu1poq28rcngYliLsuDwXXVnJsaXzCZSVuXEOMq91LqY4IGz/2QUOIDGm663ziz6Nb4IXeg==}
'@strudel/core@1.2.5':
resolution: {integrity: sha512-XO6ICbfuGfvaQ4sZmfIFsPDcjFez/+AVpNgJnmks5gtMMQNXpr0reuee61tvMiu2k5QYKzxWgzDl2FF9pJeIgA==}
'@strudel/draw@1.2.5':
resolution: {integrity: sha512-ZI4WPxUsVsxgDoeZMHo6VfFf+S1tsj8ubKBeSLcf6lH29JqPp7Nz6tPmbomOV1fzazZ7+xpkC7z772MdtgWy2g==}
'@strudel/mini@1.2.5':
resolution: {integrity: sha512-GmTWSBt49C3ogBy0hjM3bmf6K12PaJ1JJLsEvQu7j5J+lUd2n7GmLYR1UL6fntY4yVqT8ZwMhfUf6vIgUCTNbw==}
'@strudel/soundfonts@1.2.6':
resolution: {integrity: sha512-F7eBhohkCXCMo8WiufSAhD3JCfL5GTgCh9RlOJVT+1swiFeIss1vUAWJR3WjonB6moL8BiuNpcYOqO6i6cFy4A==}
'@strudel/tonal@1.2.5':
resolution: {integrity: sha512-mX3uW0nWH8Sa52HTLwnxlR5BrejFX77abeaiMbp+kQCMrX9I+XRdf8rEr7gdeInB6f/y2TzTVFpnsNsB5jLdoA==}
'@strudel/transpiler@1.2.5':
resolution: {integrity: sha512-mBZWfQBzknvnIYsjjzR8D67A+uIZ1iZ60U2WoXT21q5luXKz2c95RcTpqdx3oOGpLK9K00lDzcKjhQDJtj4OKw==}
'@strudel/web@1.2.6':
resolution: {integrity: sha512-e6VltuX90zZz8JVe1FtN23DCaXA9p8zGDakp5OuUb5kZ5hC6cySFkkOsHdGC6dJ0EAsZSCKXMihRxgk9GhBwKA==}
'@strudel/webaudio@1.2.6':
resolution: {integrity: sha512-4LnyNvyeAXYNmXQGGw2f0chIkDr9Rbwo+SK6Y0p8C3W8juDiOfKq2ihSVph95yaBBPRYFJ5Kttj1kLknRHYsog==}
'@supabase/auth-js@2.67.3':
resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==}
@@ -7094,6 +7121,9 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
superdough@1.2.6:
resolution: {integrity: sha512-zHEFfG+N4zhxJ9lUDf8p4Oj4sdGGQ5SrOPksdc0cKv5Yn0hzlNlMehEa5TkeIfDCiR27Y4H81ks7S8/CGSByhQ==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -7102,6 +7132,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
supradough@1.2.4:
resolution: {integrity: sha512-/v3cL9fq05QCQVwbttLHNZcjyyFFz5wdKvg3j6V6moRXpDlQG89l8dQtUIpQvHsxYI5sXNod9rh5bf+Y4NMGeA==}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
@@ -10019,6 +10052,79 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
'@strudel/codemirror@1.2.6(@codemirror/lint@6.8.4)':
dependencies:
'@codemirror/autocomplete': 6.18.4
'@codemirror/commands': 6.8.0
'@codemirror/lang-javascript': 6.2.2
'@codemirror/language': 6.10.8
'@codemirror/search': 6.5.8
'@codemirror/state': 6.5.1
'@codemirror/view': 6.36.2
'@lezer/highlight': 1.2.1
'@nanostores/persistent': 0.10.2(nanostores@0.11.3)
'@replit/codemirror-emacs': 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vim': 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vscode-keymap': 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@tonaljs/tonal': 4.10.0
nanostores: 0.11.3
superdough: 1.2.6
transitivePeerDependencies:
- '@codemirror/lint'
'@strudel/core@1.2.5':
dependencies:
fraction.js: 5.2.1
'@strudel/draw@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/soundfonts@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/webaudio': 1.2.6
sfumato: 0.1.2
soundfont2: 0.5.0
'@strudel/tonal@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@tonaljs/tonal': 4.10.0
chord-voicings: 0.0.1
webmidi: 3.1.12
'@strudel/transpiler@1.2.5':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 3.0.3
'@strudel/web@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/mini': 1.2.5
'@strudel/tonal': 1.2.5
'@strudel/transpiler': 1.2.5
'@strudel/webaudio': 1.2.6
'@strudel/webaudio@1.2.6':
dependencies:
'@strudel/core': 1.2.5
'@strudel/draw': 1.2.5
superdough: 1.2.6
supradough: 1.2.4
'@supabase/auth-js@2.67.3':
dependencies:
'@supabase/node-fetch': 2.6.15
@@ -10554,7 +10660,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -15665,12 +15771,18 @@ snapshots:
pirates: 4.0.6
ts-interface-checker: 0.1.13
superdough@1.2.6:
dependencies:
nanostores: 0.11.3
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {}
supradough@1.2.4: {}
tabbable@6.2.0: {}
tailwindcss@3.4.17:
+160 -465
View File
@@ -840,6 +840,31 @@ exports[`runs examples > example "amp" example index 0 1`] = `
]
`;
exports[`runs examples > example "anchor" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:E2 ]",
"[ 0/1 → 1/1 | note:C3 ]",
"[ 0/1 → 1/1 | note:E3 ]",
"[ 0/1 → 1/1 | note:G3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:C3 ]",
"[ 1/1 → 2/1 | note:G3 ]",
"[ 1/1 → 2/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:G4 ]",
"[ 2/1 → 3/1 | note:E3 ]",
"[ 2/1 → 3/1 | note:C4 ]",
"[ 2/1 → 3/1 | note:E4 ]",
"[ 2/1 → 3/1 | note:G4 ]",
"[ 2/1 → 3/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:C4 ]",
"[ 3/1 → 4/1 | note:G4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:E5 ]",
"[ 3/1 → 4/1 | note:G5 ]",
]
`;
exports[`runs examples > example "apply" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:C3 ]",
@@ -1847,6 +1872,31 @@ exports[`runs examples > example "chop" example index 0 1`] = `
]
`;
exports[`runs examples > example "chord" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:A4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:E3 ]",
"[ 1/1 → 2/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:G4 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 2/1 → 3/1 | note:D3 ]",
"[ 2/1 → 3/1 | note:A3 ]",
"[ 2/1 → 3/1 | note:D4 ]",
"[ 2/1 → 3/1 | note:Gb4 ]",
"[ 2/1 → 3/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:F3 ]",
"[ 3/1 → 4/1 | note:C4 ]",
"[ 3/1 → 4/1 | note:F4 ]",
"[ 3/1 → 4/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
]
`;
exports[`runs examples > example "chorus" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
@@ -2739,6 +2789,23 @@ exports[`runs examples > example "detune" example index 0 1`] = `
]
`;
exports[`runs examples > example "dictionary" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 room:0.5 ]",
"[ 0/1 → 1/1 | note:C4 room:0.5 ]",
"[ 0/1 → 1/1 | note:E4 room:0.5 ]",
"[ 1/1 → 2/1 | note:G3 room:0.5 ]",
"[ 1/1 → 2/1 | note:C4 room:0.5 ]",
"[ 1/1 → 2/1 | note:E4 room:0.5 ]",
"[ 2/1 → 3/1 | note:A3 room:0.5 ]",
"[ 2/1 → 3/1 | note:D4 room:0.5 ]",
"[ 2/1 → 3/1 | note:Gb4 room:0.5 ]",
"[ 3/1 → 4/1 | note:A3 room:0.5 ]",
"[ 3/1 → 4/1 | note:C4 room:0.5 ]",
"[ 3/1 → 4/1 | note:F4 room:0.5 ]",
]
`;
exports[`runs examples > example "distort" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh distort:0 ]",
@@ -6496,6 +6563,28 @@ exports[`runs examples > example "miditouch" example index 0 1`] = `
]
`;
exports[`runs examples > example "mode" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:E3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:G4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:G5 ]",
"[ 1/1 → 2/1 | note:C6 ]",
"[ 1/1 → 2/1 | note:E6 ]",
"[ 2/1 → 3/1 | note:E3 ]",
"[ 2/1 → 3/1 | note:C4 ]",
"[ 2/1 → 3/1 | note:E4 ]",
"[ 2/1 → 3/1 | note:G4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:G5 ]",
"[ 3/1 → 4/1 | note:C6 ]",
"[ 3/1 → 4/1 | note:E6 ]",
]
`;
exports[`runs examples > example "morph" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
@@ -6814,6 +6903,31 @@ exports[`runs examples > example "octave" example index 0 1`] = `
]
`;
exports[`runs examples > example "octaves" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:A4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:E3 ]",
"[ 1/1 → 2/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:G4 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 2/1 → 3/1 | note:D3 ]",
"[ 2/1 → 3/1 | note:A3 ]",
"[ 2/1 → 3/1 | note:D4 ]",
"[ 2/1 → 3/1 | note:Gb4 ]",
"[ 2/1 → 3/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:F3 ]",
"[ 3/1 → 4/1 | note:C4 ]",
"[ 3/1 → 4/1 | note:F4 ]",
"[ 3/1 → 4/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
]
`;
exports[`runs examples > example "off" example index 0 1`] = `
[
"[ -5/24 ⇜ (0/1 → 1/8) | note:62 ]",
@@ -6847,6 +6961,15 @@ exports[`runs examples > example "off" example index 0 1`] = `
]
`;
exports[`runs examples > example "offset" example index 0 1`] = `
[
"[ 0/1 → 1/1 | chord:Am offset:0 ]",
"[ 1/1 → 2/1 | chord:C offset:1 ]",
"[ 2/1 → 3/1 | chord:D offset:2 ]",
"[ 3/1 → 4/1 | chord:F offset:3 ]",
]
`;
exports[`runs examples > example "often" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh speed:0.5 ]",
@@ -9214,6 +9337,43 @@ exports[`runs examples > example "scale" example index 4 1`] = `
]
`;
exports[`runs examples > example "scale" example index 5 1`] = `
[
"[ 0/1 → 1/8 | note:C3 ]",
"[ 1/8 → 1/4 | note:C3 ]",
"[ 1/4 → 3/8 | note:D3 ]",
"[ 3/8 → 1/2 | note:Eb3 ]",
"[ 1/2 → 5/8 | note:E3 ]",
"[ 5/8 → 3/4 | note:G3 ]",
"[ 3/4 → 7/8 | note:A3 ]",
"[ 7/8 → 1/1 | note:C4 ]",
"[ 1/1 → 9/8 | note:C3 ]",
"[ 9/8 → 5/4 | note:C3 ]",
"[ 5/4 → 11/8 | note:D3 ]",
"[ 11/8 → 3/2 | note:Eb3 ]",
"[ 3/2 → 13/8 | note:E3 ]",
"[ 13/8 → 7/4 | note:G3 ]",
"[ 7/4 → 15/8 | note:A3 ]",
"[ 15/8 → 2/1 | note:C4 ]",
"[ 2/1 → 17/8 | note:C3 ]",
"[ 17/8 → 9/4 | note:C3 ]",
"[ 9/4 → 19/8 | note:D3 ]",
"[ 19/8 → 5/2 | note:Eb3 ]",
"[ 5/2 → 21/8 | note:E3 ]",
"[ 21/8 → 11/4 | note:G3 ]",
"[ 11/4 → 23/8 | note:A3 ]",
"[ 23/8 → 3/1 | note:C4 ]",
"[ 3/1 → 25/8 | note:C3 ]",
"[ 25/8 → 13/4 | note:C3 ]",
"[ 13/4 → 27/8 | note:D3 ]",
"[ 27/8 → 7/2 | note:Eb3 ]",
"[ 7/2 → 29/8 | note:E3 ]",
"[ 29/8 → 15/4 | note:G3 ]",
"[ 15/4 → 31/8 | note:A3 ]",
"[ 31/8 → 4/1 | note:C4 ]",
]
`;
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
@@ -11780,279 +11940,6 @@ exports[`runs examples > example "vib" example index 1 1`] = `
]
`;
exports[`runs examples > example "vibDuration" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibDuration:200 ]",
"[ 1/4 → 1/2 | s:hh vibDuration:0 ]",
"[ 1/2 → 3/4 | s:sd vibDuration:100 ]",
"[ 3/4 → 1/1 | s:hh vibDuration:0 ]",
"[ 1/1 → 5/4 | s:bd vibDuration:200 ]",
"[ 5/4 → 3/2 | s:hh vibDuration:0 ]",
"[ 3/2 → 7/4 | s:sd vibDuration:100 ]",
"[ 7/4 → 2/1 | s:hh vibDuration:0 ]",
"[ 2/1 → 9/4 | s:bd vibDuration:200 ]",
"[ 9/4 → 5/2 | s:hh vibDuration:0 ]",
"[ 5/2 → 11/4 | s:sd vibDuration:100 ]",
"[ 11/4 → 3/1 | s:hh vibDuration:0 ]",
"[ 3/1 → 13/4 | s:bd vibDuration:200 ]",
"[ 13/4 → 7/2 | s:hh vibDuration:0 ]",
"[ 7/2 → 15/4 | s:sd vibDuration:100 ]",
"[ 15/4 → 4/1 | s:hh vibDuration:0 ]",
]
`;
exports[`runs examples > example "vibDuration" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibDuration:200 ]",
"[ 1/4 → 1/2 | s:hh vibDuration:0 ]",
"[ 1/2 → 3/4 | s:sd vibDuration:100 ]",
"[ 3/4 → 1/1 | s:hh vibDuration:0 ]",
"[ 1/1 → 5/4 | s:bd vibDuration:200 ]",
"[ 5/4 → 3/2 | s:hh vibDuration:0 ]",
"[ 3/2 → 7/4 | s:sd vibDuration:100 ]",
"[ 7/4 → 2/1 | s:hh vibDuration:0 ]",
"[ 2/1 → 9/4 | s:bd vibDuration:200 ]",
"[ 9/4 → 5/2 | s:hh vibDuration:0 ]",
"[ 5/2 → 11/4 | s:sd vibDuration:100 ]",
"[ 11/4 → 3/1 | s:hh vibDuration:0 ]",
"[ 3/1 → 13/4 | s:bd vibDuration:200 ]",
"[ 13/4 → 7/2 | s:hh vibDuration:0 ]",
"[ 7/2 → 15/4 | s:sd vibDuration:100 ]",
"[ 15/4 → 4/1 | s:hh vibDuration:0 ]",
]
`;
exports[`runs examples > example "vibEnable" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibEnable:1 ]",
"[ 1/4 → 1/2 | s:hh vibEnable:0 ]",
"[ 1/2 → 3/4 | s:sd vibEnable:1 ]",
"[ 3/4 → 1/1 | s:hh vibEnable:0 ]",
"[ 1/1 → 5/4 | s:bd vibEnable:1 ]",
"[ 5/4 → 3/2 | s:hh vibEnable:0 ]",
"[ 3/2 → 7/4 | s:sd vibEnable:1 ]",
"[ 7/4 → 2/1 | s:hh vibEnable:0 ]",
"[ 2/1 → 9/4 | s:bd vibEnable:1 ]",
"[ 9/4 → 5/2 | s:hh vibEnable:0 ]",
"[ 5/2 → 11/4 | s:sd vibEnable:1 ]",
"[ 11/4 → 3/1 | s:hh vibEnable:0 ]",
"[ 3/1 → 13/4 | s:bd vibEnable:1 ]",
"[ 13/4 → 7/2 | s:hh vibEnable:0 ]",
"[ 7/2 → 15/4 | s:sd vibEnable:1 ]",
"[ 15/4 → 4/1 | s:hh vibEnable:0 ]",
]
`;
exports[`runs examples > example "vibEnable" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibEnable:1 ]",
"[ 1/4 → 1/2 | s:hh vibEnable:0 ]",
"[ 1/2 → 3/4 | s:sd vibEnable:1 ]",
"[ 3/4 → 1/1 | s:hh vibEnable:0 ]",
"[ 1/1 → 5/4 | s:bd vibEnable:1 ]",
"[ 5/4 → 3/2 | s:hh vibEnable:0 ]",
"[ 3/2 → 7/4 | s:sd vibEnable:1 ]",
"[ 7/4 → 2/1 | s:hh vibEnable:0 ]",
"[ 2/1 → 9/4 | s:bd vibEnable:1 ]",
"[ 9/4 → 5/2 | s:hh vibEnable:0 ]",
"[ 5/2 → 11/4 | s:sd vibEnable:1 ]",
"[ 11/4 → 3/1 | s:hh vibEnable:0 ]",
"[ 3/1 → 13/4 | s:bd vibEnable:1 ]",
"[ 13/4 → 7/2 | s:hh vibEnable:0 ]",
"[ 7/2 → 15/4 | s:sd vibEnable:1 ]",
"[ 15/4 → 4/1 | s:hh vibEnable:0 ]",
]
`;
exports[`runs examples > example "vibGamepad" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepad:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepad:1 ]",
"[ 1/2 → 3/4 | s:sd vibGamepad:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepad:1 ]",
"[ 1/1 → 5/4 | s:bd vibGamepad:0 ]",
"[ 5/4 → 3/2 | s:hh vibGamepad:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepad:0 ]",
"[ 7/4 → 2/1 | s:hh vibGamepad:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepad:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepad:1 ]",
"[ 5/2 → 11/4 | s:sd vibGamepad:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepad:1 ]",
"[ 3/1 → 13/4 | s:bd vibGamepad:0 ]",
"[ 13/4 → 7/2 | s:hh vibGamepad:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepad:0 ]",
"[ 15/4 → 4/1 | s:hh vibGamepad:1 ]",
]
`;
exports[`runs examples > example "vibGamepad" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepad:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepad:1 ]",
"[ 1/2 → 3/4 | s:sd vibGamepad:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepad:1 ]",
"[ 1/1 → 5/4 | s:bd vibGamepad:0 ]",
"[ 5/4 → 3/2 | s:hh vibGamepad:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepad:0 ]",
"[ 7/4 → 2/1 | s:hh vibGamepad:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepad:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepad:1 ]",
"[ 5/2 → 11/4 | s:sd vibGamepad:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepad:1 ]",
"[ 3/1 → 13/4 | s:bd vibGamepad:0 ]",
"[ 13/4 → 7/2 | s:hh vibGamepad:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepad:0 ]",
"[ 15/4 → 4/1 | s:hh vibGamepad:1 ]",
]
`;
exports[`runs examples > example "vibGamepadIndex" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepadIndex:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepadIndex:1 ]",
"[ 1/2 → 3/4 | s:sd vibGamepadIndex:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepadIndex:1 ]",
"[ 1/1 → 5/4 | s:bd vibGamepadIndex:0 ]",
"[ 5/4 → 3/2 | s:hh vibGamepadIndex:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepadIndex:0 ]",
"[ 7/4 → 2/1 | s:hh vibGamepadIndex:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepadIndex:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepadIndex:1 ]",
"[ 5/2 → 11/4 | s:sd vibGamepadIndex:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepadIndex:1 ]",
"[ 3/1 → 13/4 | s:bd vibGamepadIndex:0 ]",
"[ 13/4 → 7/2 | s:hh vibGamepadIndex:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepadIndex:0 ]",
"[ 15/4 → 4/1 | s:hh vibGamepadIndex:1 ]",
]
`;
exports[`runs examples > example "vibIntensity" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/4 → 1/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/2 → 3/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/4 → 1/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/4 → 3/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/2 → 7/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/4 → 2/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 9/4 → 5/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/2 → 11/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 11/4 → 3/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 13/4 → 7/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/2 → 15/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 15/4 → 4/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
]
`;
exports[`runs examples > example "vibIntensity" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/4 → 1/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/2 → 3/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/4 → 1/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/4 → 3/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/2 → 7/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/4 → 2/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 9/4 → 5/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/2 → 11/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 11/4 → 3/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 13/4 → 7/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/2 → 15/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 15/4 → 4/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
]
`;
exports[`runs examples > example "vibStrong" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:1 ]",
"[ 1/4 → 1/2 | s:hh vibStrong:0 ]",
"[ 1/2 → 3/4 | s:sd vibStrong:0.5 ]",
"[ 3/4 → 1/1 | s:hh vibStrong:0 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:1 ]",
"[ 5/4 → 3/2 | s:hh vibStrong:0 ]",
"[ 3/2 → 7/4 | s:sd vibStrong:0.5 ]",
"[ 7/4 → 2/1 | s:hh vibStrong:0 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:1 ]",
"[ 9/4 → 5/2 | s:hh vibStrong:0 ]",
"[ 5/2 → 11/4 | s:sd vibStrong:0.5 ]",
"[ 11/4 → 3/1 | s:hh vibStrong:0 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:1 ]",
"[ 13/4 → 7/2 | s:hh vibStrong:0 ]",
"[ 7/2 → 15/4 | s:sd vibStrong:0.5 ]",
"[ 15/4 → 4/1 | s:hh vibStrong:0 ]",
]
`;
exports[`runs examples > example "vibStrong" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:1 ]",
"[ 1/4 → 1/2 | s:hh vibStrong:0 ]",
"[ 1/2 → 3/4 | s:sd vibStrong:0.5 ]",
"[ 3/4 → 1/1 | s:hh vibStrong:0 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:1 ]",
"[ 5/4 → 3/2 | s:hh vibStrong:0 ]",
"[ 3/2 → 7/4 | s:sd vibStrong:0.5 ]",
"[ 7/4 → 2/1 | s:hh vibStrong:0 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:1 ]",
"[ 9/4 → 5/2 | s:hh vibStrong:0 ]",
"[ 5/2 → 11/4 | s:sd vibStrong:0.5 ]",
"[ 11/4 → 3/1 | s:hh vibStrong:0 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:1 ]",
"[ 13/4 → 7/2 | s:hh vibStrong:0 ]",
"[ 7/2 → 15/4 | s:sd vibStrong:0.5 ]",
"[ 15/4 → 4/1 | s:hh vibStrong:0 ]",
]
`;
exports[`runs examples > example "vibWeak" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibWeak:0.5 ]",
"[ 1/4 → 1/2 | s:hh vibWeak:0 ]",
"[ 1/2 → 3/4 | s:sd vibWeak:0.8 ]",
"[ 3/4 → 1/1 | s:hh vibWeak:0 ]",
"[ 1/1 → 5/4 | s:bd vibWeak:0.5 ]",
"[ 5/4 → 3/2 | s:hh vibWeak:0 ]",
"[ 3/2 → 7/4 | s:sd vibWeak:0.8 ]",
"[ 7/4 → 2/1 | s:hh vibWeak:0 ]",
"[ 2/1 → 9/4 | s:bd vibWeak:0.5 ]",
"[ 9/4 → 5/2 | s:hh vibWeak:0 ]",
"[ 5/2 → 11/4 | s:sd vibWeak:0.8 ]",
"[ 11/4 → 3/1 | s:hh vibWeak:0 ]",
"[ 3/1 → 13/4 | s:bd vibWeak:0.5 ]",
"[ 13/4 → 7/2 | s:hh vibWeak:0 ]",
"[ 7/2 → 15/4 | s:sd vibWeak:0.8 ]",
"[ 15/4 → 4/1 | s:hh vibWeak:0 ]",
]
`;
exports[`runs examples > example "vibWeak" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibWeak:0.5 ]",
"[ 1/4 → 1/2 | s:hh vibWeak:0 ]",
"[ 1/2 → 3/4 | s:sd vibWeak:0.8 ]",
"[ 3/4 → 1/1 | s:hh vibWeak:0 ]",
"[ 1/1 → 5/4 | s:bd vibWeak:0.5 ]",
"[ 5/4 → 3/2 | s:hh vibWeak:0 ]",
"[ 3/2 → 7/4 | s:sd vibWeak:0.8 ]",
"[ 7/4 → 2/1 | s:hh vibWeak:0 ]",
"[ 2/1 → 9/4 | s:bd vibWeak:0.5 ]",
"[ 9/4 → 5/2 | s:hh vibWeak:0 ]",
"[ 5/2 → 11/4 | s:sd vibWeak:0.8 ]",
"[ 11/4 → 3/1 | s:hh vibWeak:0 ]",
"[ 3/1 → 13/4 | s:bd vibWeak:0.5 ]",
"[ 13/4 → 7/2 | s:hh vibWeak:0 ]",
"[ 7/2 → 15/4 | s:sd vibWeak:0.8 ]",
"[ 15/4 → 4/1 | s:hh vibWeak:0 ]",
]
`;
exports[`runs examples > example "vibmod" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:a vib:4 vibmod:0.25 ]",
@@ -12079,198 +11966,6 @@ exports[`runs examples > example "vibmod" example index 1 1`] = `
]
`;
exports[`runs examples > example "vibrate" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:hh ]",
"[ 1/2 → 3/4 | s:sd ]",
"[ 3/4 → 1/1 | s:hh ]",
"[ 1/1 → 5/4 | s:bd ]",
"[ 5/4 → 3/2 | s:hh ]",
"[ 3/2 → 7/4 | s:sd ]",
"[ 7/4 → 2/1 | s:hh ]",
"[ 2/1 → 9/4 | s:bd ]",
"[ 9/4 → 5/2 | s:hh ]",
"[ 5/2 → 11/4 | s:sd ]",
"[ 11/4 → 3/1 | s:hh ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:hh ]",
"[ 7/2 → 15/4 | s:sd ]",
"[ 15/4 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "vibrate" example index 1 1`] = `
[
"[ 0/1 → 1/1 | s:bd vibStrong:1 vibWeak:0.5 ]",
"[ 1/1 → 2/1 | s:hh vibStrong:0.1 vibWeak:0 ]",
"[ 2/1 → 3/1 | s:sd vibStrong:0.5 vibWeak:0.8 ]",
"[ 3/1 → 4/1 | s:hh vibStrong:0 vibWeak:0 ]",
]
`;
exports[`runs examples > example "vibrate" example index 2 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:bd ]",
"[ 1/2 → 3/4 | s:bd ]",
"[ 3/4 → 1/1 | s:bd ]",
"[ 1/1 → 5/4 | s:bd ]",
"[ 5/4 → 3/2 | s:bd ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 7/4 → 2/1 | s:bd ]",
"[ 2/1 → 9/4 | s:bd ]",
"[ 9/4 → 5/2 | s:bd ]",
"[ 5/2 → 11/4 | s:bd ]",
"[ 11/4 → 3/1 | s:bd ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:bd ]",
"[ 7/2 → 15/4 | s:bd ]",
"[ 15/4 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "vibrate" example index 4 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepadIndex:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepadIndex:0 ]",
"[ 1/2 → 3/4 | s:sd vibGamepadIndex:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepadIndex:0 ]",
"[ 1/1 → 5/4 | s:bd vibGamepadIndex:1 ]",
"[ 5/4 → 3/2 | s:hh vibGamepadIndex:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepadIndex:1 ]",
"[ 7/4 → 2/1 | s:hh vibGamepadIndex:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepadIndex:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepadIndex:0 ]",
"[ 5/2 → 11/4 | s:sd vibGamepadIndex:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepadIndex:0 ]",
"[ 3/1 → 13/4 | s:bd vibGamepadIndex:1 ]",
"[ 13/4 → 7/2 | s:hh vibGamepadIndex:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepadIndex:1 ]",
"[ 15/4 → 4/1 | s:hh vibGamepadIndex:1 ]",
]
`;
exports[`runs examples > example "vibrate" example index 5 1`] = `
[
"[ 0/1 → 1/8 | s:bd vibGamepadIndex:0 ]",
"[ 1/8 → 1/4 | s:bd vibGamepadIndex:1 ]",
"[ 1/4 → 3/8 | s:bd vibGamepadIndex:0 ]",
"[ (3/8 → 7/16) ⇝ 1/2 | s:bd vibGamepadIndex:1 ]",
"[ 3/8 ⇜ (7/16 → 1/2) | s:bd vibGamepadIndex:0 ]",
"[ 1/2 → 5/8 | s:bd vibGamepadIndex:1 ]",
"[ 5/8 → 3/4 | s:bd vibGamepadIndex:0 ]",
"[ 3/4 → 7/8 | s:bd vibGamepadIndex:1 ]",
"[ 7/8 → 1/1 | s:bd vibGamepadIndex:0 ]",
"[ 1/1 → 9/8 | s:bd vibGamepadIndex:0 ]",
"[ 9/8 → 5/4 | s:bd vibGamepadIndex:1 ]",
"[ 5/4 → 11/8 | s:bd vibGamepadIndex:0 ]",
"[ (11/8 → 23/16) ⇝ 3/2 | s:bd vibGamepadIndex:1 ]",
"[ 11/8 ⇜ (23/16 → 3/2) | s:bd vibGamepadIndex:0 ]",
"[ 3/2 → 13/8 | s:bd vibGamepadIndex:1 ]",
"[ 13/8 → 7/4 | s:bd vibGamepadIndex:0 ]",
"[ 7/4 → 15/8 | s:bd vibGamepadIndex:1 ]",
"[ 15/8 → 2/1 | s:bd vibGamepadIndex:0 ]",
"[ 2/1 → 17/8 | s:bd vibGamepadIndex:0 ]",
"[ 17/8 → 9/4 | s:bd vibGamepadIndex:1 ]",
"[ 9/4 → 19/8 | s:bd vibGamepadIndex:0 ]",
"[ (19/8 → 39/16) ⇝ 5/2 | s:bd vibGamepadIndex:1 ]",
"[ 19/8 ⇜ (39/16 → 5/2) | s:bd vibGamepadIndex:0 ]",
"[ 5/2 → 21/8 | s:bd vibGamepadIndex:1 ]",
"[ 21/8 → 11/4 | s:bd vibGamepadIndex:0 ]",
"[ 11/4 → 23/8 | s:bd vibGamepadIndex:1 ]",
"[ 23/8 → 3/1 | s:bd vibGamepadIndex:0 ]",
"[ 3/1 → 25/8 | s:bd vibGamepadIndex:0 ]",
"[ 25/8 → 13/4 | s:bd vibGamepadIndex:1 ]",
"[ 13/4 → 27/8 | s:bd vibGamepadIndex:0 ]",
"[ (27/8 → 55/16) ⇝ 7/2 | s:bd vibGamepadIndex:1 ]",
"[ 27/8 ⇜ (55/16 → 7/2) | s:bd vibGamepadIndex:0 ]",
"[ 7/2 → 29/8 | s:bd vibGamepadIndex:1 ]",
"[ 29/8 → 15/4 | s:bd vibGamepadIndex:0 ]",
"[ 15/4 → 31/8 | s:bd vibGamepadIndex:1 ]",
"[ 31/8 → 4/1 | s:bd vibGamepadIndex:0 ]",
]
`;
exports[`runs examples > example "vibrate" example index 6 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepad:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepad:0 ]",
"[ 1/2 → 3/4 | s:sd vibGamepad:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepad:0 ]",
"[ 1/1 → 5/4 | s:bd vibGamepad:1 ]",
"[ 5/4 → 3/2 | s:hh vibGamepad:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepad:1 ]",
"[ 7/4 → 2/1 | s:hh vibGamepad:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepad:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepad:0 ]",
"[ 5/2 → 11/4 | s:sd vibGamepad:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepad:0 ]",
"[ 3/1 → 13/4 | s:bd vibGamepad:1 ]",
"[ 13/4 → 7/2 | s:hh vibGamepad:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepad:1 ]",
"[ 15/4 → 4/1 | s:hh vibGamepad:1 ]",
]
`;
exports[`runs examples > example "vibrate" example index 7 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 1/4 → 1/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 1/2 → 3/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 3/4 → 1/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 1/1 → 5/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 5/4 → 3/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 3/2 → 7/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 7/4 → 2/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 2/1 → 9/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 9/4 → 5/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 5/2 → 11/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 11/4 → 3/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 3/1 → 13/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 13/4 → 7/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 7/2 → 15/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 15/4 → 4/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
]
`;
exports[`runs examples > example "vibrate" example index 8 1`] = `
[
"[ 0/1 → 1/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 1/8 → 1/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 1/4 → 3/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 3/8 → 1/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 1/2 → 5/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 5/8 → 3/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 3/4 → 7/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 7/8 → 1/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 1/1 → 9/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 9/8 → 5/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 5/4 → 11/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 11/8 → 3/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 3/2 → 13/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 13/8 → 7/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 7/4 → 15/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 15/8 → 2/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 2/1 → 17/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 17/8 → 9/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 9/4 → 19/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 19/8 → 5/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 5/2 → 21/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 21/8 → 11/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 11/4 → 23/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 23/8 → 3/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 3/1 → 25/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 25/8 → 13/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 13/4 → 27/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 27/8 → 7/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 7/2 → 29/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 29/8 → 15/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 15/4 → 31/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 31/8 → 4/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
]
`;
exports[`runs examples > example "voicing" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:52 ]",
File diff suppressed because it is too large Load Diff
-8
View File
@@ -18,14 +18,6 @@ const skippedExamples = [
'accelerationZ',
'accelerationY',
'accelerationX',
'vibrate',
'vibGamepadIndex',
'vibGamepadId',
'vibEnable',
'vibWeak',
'vibStrong',
'vibDuration',
'vibIntensity',
'defaultmidimap',
'midimaps',
];
+2 -2
View File
@@ -23,7 +23,7 @@ import '@strudel/xen/xen.mjs';
import '../website/src/repl/piano';
//import * as motionHelpers from '../packages/motion/index.mjs';
//import * as geolocationHelpers from '../packages/geolocation/index.mjs';
//import * as gamepadHelpers from '../packages/gamepad/index.mjs';
import * as gamepadHelpers from '../packages/gamepad/index.mjs';
class MockedNode {
chain() {
@@ -141,8 +141,8 @@ evalScope(
uiHelpersMocked,
webaudio,
tonalHelpers,
/*
gamepadHelpers,
/*
toneHelpers,
voicingHelpers,
drawHelpers,
+15 -1
View File
@@ -4,11 +4,25 @@ import { describe, it } from 'vitest';
const tuneKeys = Object.keys(tunes);
// Node 24 tightened Number→string rounding; clamp decimals so snapshots stay stable across engines.
const roundFloatStrings = (input, precision = 12) => {
// if matches a decimal number ex: 12.34, -0.5, 0.123, 99.0, 1.932093850293
const regex = /-?\d+\.\d+/g;
return input.replace(regex, (match) => {
// converts the literal to a number, performs round to nearest (ties to even)
// at the requested precision, and returns the rounded decimal string
const rounded = Number(match).toFixed(precision);
// trims trailing zeros (and a dangling dot) after rounding, so the displayed string looks tidy
return rounded.replace(/\.?0+$/, '').replace(/\.$/, '');
});
};
describe('renders tunes', () => {
tuneKeys.forEach((key) => {
it(`tune: ${key}`, async ({ expect }) => {
const haps = await queryCode(tunes[key], testCycles[key] || 1);
expect(haps).toMatchSnapshot();
const normalized = haps.map((hap) => roundFloatStrings(hap));
expect(normalized).toMatchSnapshot();
});
});
});
+3
View File
@@ -1,5 +1,8 @@
{
"dependencies": {
"csv": "^6.3.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+3
View File
@@ -75,5 +75,8 @@
"sharp": "^0.33.5",
"workbox-window": "^7.3.0",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -63,7 +63,7 @@ You can also create custom aliases for existing sounds using the `soundAlias` fu
<MiniRepl
client:idle
tune={`soundAlias("RolandTR808_bd", "kick")
tune={`soundAlias('RolandTR808_bd', 'kick')
s("kick")`}
/>
@@ -32,7 +32,7 @@ There are 3 quick ways to embed strudel in your website:
### Inside an iframe
Using an iframe is the most easy way to embed a studel tune.
Using an iframe is the most easy way to embed a strudel tune.
You can embed any pattern of your choice via an iframe and the URL of the pattern of your choice:
```html
@@ -133,7 +133,7 @@ If you'd rather use your own UI, you can use the `@strudel/web` package:
</script>
```
For more info on this package, see the [@strudel/web README]https://codeberg.org/uzu/strudel/src/branch/main/packages/web#strudel-web).
For more info on this package, see the [@strudel/web README](https://codeberg.org/uzu/strudel/src/branch/main/packages/web#strudel-web).
## Via npm
@@ -79,13 +79,11 @@ const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const autoResetPatternOnChange = !isUdels();
function UserPatterns({ context }) {
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternFilter } = useSettings();
const { userPatterns, patternFilter, patternAutoStart } = useSettings();
const viewingPatternID = viewingPatternData?.id;
return (
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
@@ -135,13 +133,13 @@ function UserPatterns({ context }) {
<div className="overflow-auto h-full bg-background p-2 rounded-md">
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) =>
updateCodeWindow(
context,
{ ...userPatterns[id], collection: userPattern.collection },
autoResetPatternOnChange,
)
}
onClick={(id) => {
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
patterns={userPatterns}
started={context.started}
activePattern={activePattern}
@@ -188,17 +186,14 @@ function FeaturedPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.featured);
const { patternAutoStart } = useSettings();
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={featuredPageNum}
patternOnClick={(id) => {
updateCodeWindow(
context,
{ ...patterns[id], collection: patternFilterName.featured },
autoResetPatternOnChange,
);
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.featured }, patternAutoStart);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetFeaturedPatterns(pageNum - 1);
@@ -213,13 +208,14 @@ function LatestPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.public);
const { patternAutoStart } = useSettings();
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={latestPageNum}
patternOnClick={(id) => {
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, patternAutoStart);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetPublicPatterns(pageNum - 1);
@@ -112,6 +112,7 @@ export function SettingsTab({ started }) {
multiChannelOrbits,
isTabIndentationEnabled,
isMultiCursorEnabled,
patternAutoStart,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -304,6 +305,11 @@ export function SettingsTab({ started }) {
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
value={isCSSAnimationDisabled}
/>
<Checkbox
label="Auto-start pattern on pattern change"
onChange={(cbEvent) => settingsMap.setKey('patternAutoStart', cbEvent.target.checked)}
value={patternAutoStart}
/>
</FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings">
@@ -62,11 +62,9 @@ export function SoundsTab() {
// stop current sound on mouseup
useEvent('mouseup', () => {
const t = trigRef.current;
const ref = trigRef.current;
trigRef.current = undefined;
t?.then((ref) => {
ref?.stop(getAudioContext().currentTime + 0.01);
});
ref?.stop?.(getAudioContext().currentTime + 0.01);
});
return (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
@@ -124,12 +122,19 @@ export function SoundsTab() {
duration: 0.5,
};
soundPreviewIdx++;
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
trigRef.current.then((ref) => {
connectToDestination(ref?.node);
});
try {
// Pre-load the sample by calling onTrigger with a future time
// This triggers the loading but schedules playback for later
const time = ctx.currentTime + 0.5; // Give 500ms for loading
const ref = await onTrigger(time, params, onended);
trigRef.current = ref;
if (ref?.node) {
connectToDestination(ref.node);
}
} catch (err) {
console.warn('Failed to trigger sound:', err);
}
}}
>
{' '}
+1 -5
View File
@@ -92,11 +92,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
async function blobToDataUrl(blob) {
return new Promise((resolve) => {
var reader = new FileReader();
reader.onload = function (event) {
resolve(event.target.result);
};
reader.readAsDataURL(blob);
resolve(URL.createObjectURL(blob));
});
}
+5
View File
@@ -96,6 +96,11 @@ export function useSettings() {
isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
patternAutoStart: isUdels()
? false
: state.patternAutoStart === undefined
? true
: parseBoolean(state.patternAutoStart),
};
}