mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-20 04:27:05 -04:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 301fb5e59a | |||
| 9ac691c19e | |||
| 6e7512016c | |||
| 3bbd808783 | |||
| 7b245abd1a | |||
| 9e5c9c7539 | |||
| d14194db6a | |||
| 0960741156 | |||
| bbb977afcc | |||
| 81a4981a68 | |||
| 90d82c8988 | |||
| 15f7b7c1a2 | |||
| b6e382508f | |||
| 3e7ae88043 | |||
| 897a2016d0 | |||
| 7a1c1a75b1 | |||
| 7f06f11e96 | |||
| 5acb00795e | |||
| f4b68fa2e6 | |||
| 1b37f1ed1d | |||
| f8943dbc94 | |||
| a3f82f90a9 | |||
| 670f75b7e8 | |||
| 79875ebc50 | |||
| a1bd905c06 | |||
| 247bf9b930 | |||
| 8f24ce223f | |||
| 51648d3e6e | |||
| d4e67e1c20 | |||
| edf4739cc6 | |||
| 3e636bd6f0 | |||
| 4df2cc3086 | |||
| 809eb7132c | |||
| dd6429007e | |||
| e211d5a6bb | |||
| 404d9999f3 | |||
| bc1c83ed64 | |||
| 91e1cc136e | |||
| 92e3a80e65 | |||
| 4f046ae883 | |||
| 2b575d50b9 | |||
| 7b92874909 | |||
| 10bfe32a2d | |||
| a41b07c67a | |||
| 20027d7208 | |||
| f38792c569 | |||
| 2c2bfd7070 | |||
| 484c5e6cdd | |||
| a036c4f921 | |||
| c0e7971be7 | |||
| b491c42af5 | |||
| ee43d85f29 | |||
| fcd83cce55 | |||
| fddf2ff2a7 | |||
| 6d4787ee33 | |||
| 1a362bd8ea | |||
| 20bbda30f8 | |||
| a8c613b7eb | |||
| fd0a910bfb | |||
| ac7e9c6978 | |||
| c13a726129 | |||
| 99b3c04a76 | |||
| 57fac57677 | |||
| a3b803c3f3 | |||
| 4d39f36868 | |||
| c64aa3bf01 | |||
| a0c2edad30 | |||
| 82c9047195 |
@@ -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,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({
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel/web": "workspace:*"
|
||||
"@strudel/web": "1.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,5 +32,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -73,5 +73,8 @@
|
||||
"prettier": "^3.4.2",
|
||||
"vitest": "^3.0.4",
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -54,5 +54,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+198
-6
@@ -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']);
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,5 +37,8 @@
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -33,5 +33,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,5 +33,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -34,5 +34,8 @@
|
||||
"devDependencies": {
|
||||
"tree-sitter-haskell": "^0.23.1",
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,5 +40,8 @@
|
||||
"devDependencies": {
|
||||
"pkg": "^5.8.1",
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+59
-42
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +35,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,8 @@
|
||||
"peggy": "^4.2.0",
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,8 @@
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,5 +40,8 @@
|
||||
"mondo": "*",
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,5 +34,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,5 +44,8 @@
|
||||
"devDependencies": {
|
||||
"pkg": "^5.8.1",
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,8 @@
|
||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,5 +48,8 @@
|
||||
"@rollup/plugin-replace": "^6.0.2",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,8 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"cowsay": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,5 +37,8 @@
|
||||
"devDependencies": {
|
||||
"node-fetch": "^3.3.2",
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -37,5 +37,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"nanostores": "^0.11.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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]]));
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -25,5 +25,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,8 @@
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
```
|
||||
|
||||
@@ -43,5 +43,8 @@
|
||||
"@rollup/plugin-replace": "^6.0.2",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,5 +40,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +35,8 @@
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
Generated
+132
-20
@@ -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:
|
||||
|
||||
@@ -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 ]",
|
||||
|
||||
+6336
-6336
File diff suppressed because it is too large
Load Diff
@@ -18,14 +18,6 @@ const skippedExamples = [
|
||||
'accelerationZ',
|
||||
'accelerationY',
|
||||
'accelerationX',
|
||||
'vibrate',
|
||||
'vibGamepadIndex',
|
||||
'vibGamepadId',
|
||||
'vibEnable',
|
||||
'vibWeak',
|
||||
'vibStrong',
|
||||
'vibDuration',
|
||||
'vibIntensity',
|
||||
'defaultmidimap',
|
||||
'midimaps',
|
||||
];
|
||||
|
||||
+2
-2
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"csv": "^6.3.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,5 +75,8 @@
|
||||
"sharp": "^0.33.5",
|
||||
"workbox-window": "^7.3.0",
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user