Compare commits

..

8 Commits

Author SHA1 Message Date
froos b48321a84c Merge branch 'main' into nkymut/gamepad-pr 2025-10-28 23:32:28 +01:00
froos 827b659592 Merge branch 'main' into nkymut/gamepad-pr 2025-10-20 20:42:00 +02:00
nkymut 4553408b28 fix custom button mapping error 2025-05-21 01:18:44 +08:00
nkymut 42eb33440c Improve logger messages and connection error handling 2025-05-21 01:00:58 +08:00
nkymut 87768198a0 fix import logger 2025-05-17 16:10:11 +08:00
nkymut c24a87df48 codeformat 2025-05-17 16:06:06 +08:00
nkymut 2d63f36201 update gamepad documentation for button mapping 2025-05-17 16:03:57 +08:00
nkymut 43ac2d3d72 Add gamepad button mapping support 2025-05-17 15:41:48 +08:00
69 changed files with 6735 additions and 7301 deletions
-1
View File
@@ -1 +0,0 @@
22
+1 -1
View File
@@ -13,7 +13,7 @@ https://strudel.cc/
After cloning the project, you can run the REPL locally:
1. Install [Node.js](https://nodejs.org/) 18 or newer
1. Install [Node.js](https://nodejs.org/)
2. Install [pnpm](https://pnpm.io/installation)
3. Install dependencies by running the following command:
```bash
+1 -1
View File
@@ -1,4 +1,4 @@
<script src="https://unpkg.com/@strudel/web@1.2.6"></script>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">PLAY</button>
<script>
initStrudel({
+8 -11
View File
@@ -12,16 +12,13 @@
"vite": "^6.0.11"
},
"dependencies": {
"@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"
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
}
}
+8 -5
View File
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
samples('github:felixroos/samples')
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('https://strudel.cc/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,12 +28,13 @@ samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/ti
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(4/3)
).fast(2/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
samples('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
@@ -60,14 +61,16 @@ stack(
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12).fast(2)
.late("[0 .05]*2").late(12)
`;
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('https://strudel.b-cdn.net/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
samples('github:felixroos/samples')
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
+1 -1
View File
@@ -13,7 +13,7 @@
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel, hush } from '@strudel/web';
import { initStrudel } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
+1 -4
View File
@@ -13,9 +13,6 @@
"vite": "^6.0.11"
},
"dependencies": {
"@strudel/web": "1.2.6"
},
"engines": {
"node": ">=18.0.0"
"@strudel/web": "workspace:*"
}
}
-3
View File
@@ -18,8 +18,5 @@
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@strudel/tonal": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -13,8 +13,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -32,8 +32,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -4
View File
@@ -73,8 +73,5 @@
"prettier": "^3.4.2",
"vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
}
+10 -14
View File
@@ -1,10 +1,8 @@
import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
//TODO: fix tonal scale import
// import { Scale } from '@tonaljs/tonal';
// import { soundMap } from '@strudel/webaudio';
let soundMap = undefined;
import { Scale } from '@tonaljs/tonal';
import { soundMap } from 'superdough';
import { complex } from '@strudel/tonal';
const escapeHtml = (str) => {
@@ -81,9 +79,7 @@ const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export function bankCompletions() {
// TODO: FIX IMPORT
const soundDict = soundMap?.get() ?? {};
const soundDict = soundMap.get();
const banks = new Set();
for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_');
@@ -94,13 +90,13 @@ export function bankCompletions() {
.map((name) => ({ label: name, type: 'bank' }));
}
// Attempt to get all scale names from Tonal TODO: FIX IMPORT
// Attempt to get all scale names from Tonal
let scaleCompletions = [];
// try {
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
// } catch (e) {
// console.warn('[autocomplete] Could not load scale names from Tonal:', e);
// }
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 = [
@@ -272,7 +268,7 @@ function soundHandler(context) {
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap?.get() ?? {}).sort();
const soundNames = Object.keys(soundMap.get()).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length;
-3
View File
@@ -54,8 +54,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+6 -198
View File
@@ -1288,151 +1288,6 @@ 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.
*
@@ -1895,64 +1750,17 @@ export const { semitone } = registerControl('semitone');
// TODO: synth param
export const { voice } = registerControl('voice');
// voicings // https://codeberg.org/uzu/strudel/issues/506
/**
* 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()
**/
// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
export const { chord } = registerControl('chord');
/**
* 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)
**/
// which dictionary to use for the voicings
export const { dictionary, dict } = registerControl('dictionary', 'dict');
/** 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()
**/
// the top note to align the voicing to, defaults to c5
export const { anchor } = registerControl('anchor');
/**
* 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
**/
// how the voicing is offset from the anchored position
export const { offset } = registerControl('offset');
/**
* 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()
**/
// how many octaves are voicing steps spread apart, defaults to 1
export const { octaves } = registerControl('octaves');
/**
* 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()
*
**/
// below = anchor note will be removed from the voicing, useful for melody harmonization
export const { mode } = registerControl(['mode', 'anchor']);
/**
-3
View File
@@ -37,8 +37,5 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -38,8 +38,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -4
View File
@@ -25,8 +25,5 @@
"@strudel/core": "workspace:*",
"@tauri-apps/api": "^2.2.0"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
"homepage": "https://codeberg.org/uzu/strudel#readme"
}
+2 -2
View File
@@ -1,9 +1,9 @@
# @strudel/draw
# @strudel/canvas
Helpers for drawing with the Canvas API and Strudel
## Install
```sh
npm i @strudel/draw --save
npm i @strudel/canvas --save
```
-3
View File
@@ -33,8 +33,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -4
View File
@@ -20,8 +20,5 @@
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"engines": {
"node": ">=18.0.0"
}
"homepage": "https://codeberg.org/uzu/strudel#readme"
}
+7 -7
View File
@@ -14,7 +14,12 @@ npm i @strudel/gamepad --save
import { gamepad } from '@strudel/gamepad';
// Initialize gamepad (optional index parameter, defaults to 0)
const pad = gamepad(0);
const pad = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
//const pad = gamepad(0, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
// Use gamepad inputs in patterns
const pattern = sequence([
@@ -40,12 +45,6 @@ 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
@@ -92,6 +91,7 @@ $: 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
+24 -5
View File
@@ -29,10 +29,6 @@ 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
@@ -111,9 +107,32 @@ $: s("free_hadouken -").slow(2)
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
`} />
## Multiple Gamepads
### Button Sequences
### Button Mappings
The gamepad module supports different button mapping configurations to accommodate various gamepad layouts:
- **XBOX** (Default): Standard Xbox-style mapping where A=0, B=1, X=2, Y=3
- **NES**: Nintendo-style mapping where B=0, A=1, Y=2, X=3
- **Custom**: Define your own button mapping by passing an object with button assignments
You can specify the mapping when initializing the gamepad:
<MiniRepl
client:idle
tune={`
const pad1 = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
const pad2 = gamepad(1, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
const pad3 = gamepad(2, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
const pad4 = gamepad(3, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
`}
/>
### Multiple Gamepads
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
<MiniRepl
client:idle
+119 -32
View File
@@ -1,39 +1,62 @@
// @strudel/gamepad/index.mjs
import { signal } from '@strudel/core';
import { logger } from '@strudel/core';
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
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,
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,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
},
NES: {
// Nintendo mapping
a: 1,
b: 0,
x: 3,
y: 2,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
},
};
class ButtonSequenceDetector {
constructor(timeWindow = 1000) {
constructor(timeWindow = 1000, mapping) {
this.sequence = [];
this.timeWindow = timeWindow;
this.lastInputTime = 0;
this.buttonStates = Array(16).fill(0); // Track previous state of each button
this.buttonMap = mapping;
// Button mapping for character inputs
}
@@ -48,7 +71,8 @@ class ButtonSequenceDetector {
}
// Store the button name instead of index
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
const buttonName =
Object.keys(this.buttonMap).find((key) => this.buttonMap[key] === buttonIndex) || buttonIndex.toString();
this.sequence.push({
input: buttonName,
@@ -91,9 +115,9 @@ class ButtonSequenceDetector {
// Check if either the input matches directly or they refer to the same button in the map
return (
input === target ||
buttonMap[input] === buttonMap[target] ||
this.buttonMap[input] === this.buttonMap[target] ||
// Also check if the numerical index matches
buttonMap[input] === parseInt(target)
this.buttonMap[input] === parseInt(target)
);
})
? 1
@@ -102,9 +126,10 @@ class ButtonSequenceDetector {
}
class GamepadHandler {
constructor(index = 0) {
constructor(index = 0, mapping) {
// Add index parameter
this._gamepads = {};
this._mapping = mapping;
this._activeGamepad = index; // Use provided index
this._axes = [0, 0, 0, 0];
this._buttons = Array(16).fill(0);
@@ -147,12 +172,66 @@ class GamepadHandler {
}
}
// Add utility function to list all connected gamepads
export const listGamepads = () => {
const gamepads = navigator.getGamepads();
const connectedGamepads = Array.from(gamepads)
.filter((gp) => gp !== null)
.map((gp) => ({
index: gp.index,
id: gp.id,
mapping: gp.mapping,
buttons: gp.buttons.length,
axes: gp.axes.length,
connected: gp.connected,
timestamp: gp.timestamp,
}));
// Format the gamepads info into a readable string
const gamepadsInfo = connectedGamepads.map((gp) => `${gp.index}: ${gp.id}`).join('\n');
logger(`[gamepad] available gamepads:\n${gamepadsInfo}`);
return connectedGamepads;
};
// Module-level state store for toggle states
const gamepadStates = new Map();
export const gamepad = (index = 0) => {
const handler = new GamepadHandler(index);
const sequenceDetector = new ButtonSequenceDetector(2000);
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; // Standard gamepad has 16 buttons
for (const [key, value] of Object.entries(mapping)) {
if (typeof value !== 'number' || value < 0 || value >= maxButtons) {
throw new Error(
`[gamepad] invalid button mapping for '${key}': ${value}. Must be a number between 0 and ${maxButtons - 1}`,
);
}
}
}
if (!buttonMap) {
throw new Error(`[gamepad] button mapping '${mapping}' not found`);
}
const handler = new GamepadHandler(index, buttonMap);
const sequenceDetector = new ButtonSequenceDetector(2000, buttonMap);
// Base signal that polls gamepad state and handles sequence detection
const baseSignal = signal((t) => {
@@ -222,8 +301,14 @@ export const gamepad = (index = 0) => {
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
};
const checkSequence = btnSequence;
const sequence = btnSequence;
const btnSeq = btnSequence;
const btnseq = btnSeq;
const btnseq = btnSequence;
const seq = btnSequence;
logger(
`[gamepad] connected to gamepad ${index} (${requestedGamepad.id}) with ${typeof mapping === 'object' ? 'custom' : mapping} mapping`,
);
// Return an object with all controls
return {
@@ -238,9 +323,11 @@ export const gamepad = (index = 0) => {
]),
),
checkSequence,
sequence,
btnSequence,
btnSeq,
btnseq,
seq,
raw: baseSignal,
};
};
-3
View File
@@ -33,8 +33,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -34,8 +34,5 @@
"devDependencies": {
"tree-sitter-haskell": "^0.23.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -40,8 +40,5 @@
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+42 -59
View File
@@ -5,10 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { Pattern, getEventOffsetMs, 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;
@@ -191,7 +190,7 @@ function mapCC(mapping, value) {
}
// sends a cc message to the given device on the given channel
function sendCC(ccn, ccv, device, midichan, targetTime) {
function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -199,23 +198,19 @@ function sendCC(ccn, ccv, device, midichan, targetTime) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
scheduleAtTime(() => {
device.sendControlChange(ccn, scaled, midichan);
}, targetTime);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
// sends a program change message to the given device on the given channel
function sendProgramChange(progNum, device, midichan, targetTime) {
function sendProgramChange(progNum, device, midichan, timeOffsetString) {
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
throw new Error('expected progNum (program change) to be a number between 0 and 127');
}
scheduleAtTime(() => {
device.sendProgramChange(progNum, midichan);
}, targetTime);
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
}
// sends a sysex message to the given device on the given channel
function sendSysex(sysexid, sysexdata, device, targetTime) {
function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
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');
@@ -230,13 +225,11 @@ function sendSysex(sysexid, sysexdata, device, targetTime) {
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysex bytes must be integers between 0 and 255');
}
scheduleAtTime(() => {
device.sendSysex(sysexid, sysexdata);
}, targetTime);
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
}
// sends a NRPN message to the given device on the given channel
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
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');
@@ -244,34 +237,28 @@ function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
} 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');
}
scheduleAtTime(() => {
device.sendNRPN(nrpnn, nrpv, midichan);
}, targetTime);
device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
}
// sends a pitch bend message to the given device on the given channel
function sendPitchBend(midibend, device, midichan, targetTime) {
function sendPitchBend(midibend, device, midichan, timeOffsetString) {
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
throw new Error('expected midibend to be a number between -1 and 1');
}
scheduleAtTime(() => {
device.sendPitchBend(midibend, midichan);
}, targetTime);
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
}
// sends a channel aftertouch message to the given device on the given channel
function sendAftertouch(miditouch, device, midichan, targetTime) {
function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
throw new Error('expected miditouch to be a number between 0 and 1');
}
scheduleAtTime(() => {
device.sendChannelAftertouch(miditouch, midichan);
}, targetTime);
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
}
// sends a note message to the given device on the given channel
function sendNote(note, velocity, duration, device, midichan, targetTime) {
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
if (note == null || note === '') {
throw new Error('note cannot be null or empty');
}
@@ -281,12 +268,12 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
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 });
scheduleAtTime(() => {
device.playNote(midiNote, midichan);
}, targetTime);
device.playNote(midiNote, midichan, {
time: timeOffsetString,
});
}
/**
@@ -322,6 +309,7 @@ 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
@@ -345,13 +333,18 @@ 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,
@@ -387,7 +380,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, targetTime));
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
@@ -399,12 +392,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, targetTime);
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
}
// Handle program change
if (progNum !== undefined) {
sendProgramChange(progNum, device, midichan, targetTime);
sendProgramChange(progNum, device, midichan, timeOffsetString);
}
// Handle sysex
@@ -414,63 +407,53 @@ 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, targetTime);
sendSysex(sysexid, sysexdata, device, timeOffsetString);
}
// Handle control change
if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, targetTime);
sendCC(ccn, ccv, device, midichan, timeOffsetString);
}
// Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, targetTime);
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
}
// Handle midibend
if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, targetTime);
sendPitchBend(midibend, device, midichan, timeOffsetString);
}
// Handle miditouch
if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, targetTime);
sendAftertouch(miditouch, device, midichan, timeOffsetString);
}
// Handle midicmd
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
device.sendStart({ time: timeOffsetString });
}
if (['clock', 'midiClock'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendClock();
}, targetTime);
device.sendClock({ time: timeOffsetString });
} else if (['start'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendStart();
}, targetTime);
device.sendStart({ time: timeOffsetString });
} else if (['stop'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendStop();
}, targetTime);
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
scheduleAtTime(() => {
device.sendContinue();
}, targetTime);
device.sendContinue({ time: timeOffsetString });
} else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, targetTime);
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
} else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime);
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
}
} else if (midicmd[0] === 'sysex') {
if (midicmd.length === 3) {
const [_, id, data] = midicmd;
sendSysex(id, data, device, targetTime);
sendSysex(id, data, device, timeOffsetString);
}
}
}
-3
View File
@@ -35,8 +35,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -38,8 +38,5 @@
"peggy": "^4.2.0",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -33,8 +33,5 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -40,8 +40,5 @@
"mondo": "*",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -33,8 +33,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -34,8 +34,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -44,8 +44,5 @@
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -33,8 +33,5 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -48,8 +48,5 @@
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -15,8 +15,5 @@
"type": "module",
"dependencies": {
"cowsay": "^1.6.0"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -33,8 +33,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -37,8 +37,5 @@
"devDependencies": {
"node-fetch": "^3.3.2",
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+36 -62
View File
@@ -154,24 +154,6 @@ 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;
@@ -188,58 +170,55 @@ 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);
}
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
let lfo;
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, 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;
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;
if (model === 'ladder') {
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
filter = getWorklet(context, 'ladder-processor', { frequency, q: 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 = [...envelopeValues, env].some((v) => v !== undefined);
// Apply ADSR to filter frequency
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 ** (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;
// envelope is active when any of these values is set
const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv;
// 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;
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;
}
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
getParamLfo(context, frequencyParam, start, end, lfoValues);
return filter;
}
@@ -301,11 +280,6 @@ export function getVibratoOscillator(param, value, t) {
return vibratoOscillator;
}
}
export function scheduleAtTime(callback, targetTime, audioContext = getAudioContext()) {
const currentTime = audioContext.currentTime;
webAudioTimeout(audioContext, callback, currentTime, targetTime);
}
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
-3
View File
@@ -37,8 +37,5 @@
},
"dependencies": {
"nanostores": "^0.11.3"
},
"engines": {
"node": ">=18.0.0"
}
}
+69 -69
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
import { nanFallback, _mod, cycleToSeconds } 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,6 +146,11 @@ 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,
@@ -410,7 +415,32 @@ 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,
@@ -535,87 +565,57 @@ 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 (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);
if (cutoff !== undefined) {
let lp = () =>
createFilter(
ac,
'lowpass',
cutoff,
resonance,
lpattack,
lpdecay,
lpsustain,
lprelease,
lpenv,
t,
end,
fanchor,
ftype,
drive,
);
chain.push(lp());
if (ftype === '24db') {
chain.push(lp());
}
}
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);
if (hcutoff !== undefined) {
let hp = () =>
createFilter(
ac,
'highpass',
hcutoff,
hresonance,
hpattack,
hpdecay,
hpsustain,
hprelease,
hpenv,
t,
end,
fanchor,
);
chain.push(hp());
if (ftype === '24db') {
chain.push(hp());
}
}
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);
if (bandf !== undefined) {
let bp = () =>
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
-5
View File
@@ -109,8 +109,3 @@ 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]]));
};
+2 -4
View File
@@ -1,12 +1,11 @@
// this is dough, the superdough without dependencies
// @ts-nocheck
// @ts-check
// @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);
@@ -152,8 +151,7 @@ export class TwoPoleFilter {
resonance = Math.max(resonance, 0);
cutoff = Math.min(cutoff, 20000);
let c = 2 * Math.sin(cutoff * PI_DIV_SR);
c = clamp(c, 0, 1.14); // this line prevents instability TODO: test
const c = 2 * Math.sin(cutoff * PI_DIV_SR);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c;
+3 -6
View File
@@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://codeberg.org/uzu/strudel.git"
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
@@ -25,15 +25,12 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"engines": {
"node": ">=18.0.0"
}
}
-16
View File
@@ -1,16 +0,0 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
target: 'esnext',
},
});
-3
View File
@@ -25,8 +25,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -38,8 +38,5 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
-5
View File
@@ -229,9 +229,6 @@ 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
@@ -253,8 +250,6 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* .s("piano")
* @example
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
* @example
* n("[0 0] [1 2] [3 4] [5 6]").scale("C:major:blues")
*/
export const scale = register(
'scale',
-3
View File
@@ -39,8 +39,5 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
@@ -12,8 +12,5 @@
"type": "module",
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ initStrudel();
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
document.getElementById('stop').addEventListener('click',
document.getElementById('play').addEventListener('stop',
() => hush()
);
```
-3
View File
@@ -43,8 +43,5 @@
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -40,8 +40,5 @@
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -35,8 +35,5 @@
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"engines": {
"node": ">=18.0.0"
}
}
+1
View File
@@ -41,6 +41,7 @@ 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){
+20 -132
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4)
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -85,29 +85,29 @@ importers:
examples/codemirror-repl:
dependencies:
'@strudel/codemirror':
specifier: 1.2.6
version: 1.2.6(@codemirror/lint@6.8.4)
specifier: workspace:*
version: link:../../packages/codemirror
'@strudel/core':
specifier: 1.2.5
version: 1.2.5
specifier: workspace:*
version: link:../../packages/core
'@strudel/draw':
specifier: 1.2.5
version: 1.2.5
specifier: workspace:*
version: link:../../packages/draw
'@strudel/mini':
specifier: 1.2.5
version: 1.2.5
specifier: workspace:*
version: link:../../packages/mini
'@strudel/soundfonts':
specifier: 1.2.6
version: 1.2.6
specifier: workspace:*
version: link:../../packages/soundfonts
'@strudel/tonal':
specifier: 1.2.5
version: 1.2.5
specifier: workspace:*
version: link:../../packages/tonal
'@strudel/transpiler':
specifier: 1.2.5
version: 1.2.5
specifier: workspace:*
version: link:../../packages/transpiler
'@strudel/webaudio':
specifier: 1.2.6
version: 1.2.6
specifier: workspace:*
version: link:../../packages/webaudio
devDependencies:
vite:
specifier: ^6.0.11
@@ -116,8 +116,8 @@ importers:
examples/headless-repl:
dependencies:
'@strudel/web':
specifier: 1.2.6
version: 1.2.6
specifier: workspace:*
version: link:../../packages/web
devDependencies:
vite:
specifier: ^6.0.11
@@ -2544,33 +2544,6 @@ 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==}
@@ -7121,9 +7094,6 @@ 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'}
@@ -7132,9 +7102,6 @@ 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==}
@@ -10052,79 +10019,6 @@ 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
@@ -10660,7 +10554,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -15771,18 +15665,12 @@ snapshots:
pirates: 4.0.6
ts-interface-checker: 0.1.13
superdough@1.2.6:
dependencies:
nanostores: 0.11.3
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {}
supradough@1.2.4: {}
tabbable@6.2.0: {}
tailwindcss@3.4.17:
-160
View File
@@ -840,31 +840,6 @@ 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 ]",
@@ -1872,31 +1847,6 @@ 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 ]",
@@ -2789,23 +2739,6 @@ 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 ]",
@@ -6563,28 +6496,6 @@ 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 ]",
@@ -6903,31 +6814,6 @@ 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 ]",
@@ -6961,15 +6847,6 @@ 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 ]",
@@ -9337,43 +9214,6 @@ 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 ]",
File diff suppressed because it is too large Load Diff
+1 -15
View File
@@ -4,25 +4,11 @@ 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);
const normalized = haps.map((hap) => roundFloatStrings(hap));
expect(normalized).toMatchSnapshot();
expect(haps).toMatchSnapshot();
});
});
});
-3
View File
@@ -1,8 +1,5 @@
{
"dependencies": {
"csv": "^6.3.11"
},
"engines": {
"node": ">=18.0.0"
}
}
-3
View File
@@ -75,8 +75,5 @@
"sharp": "^0.33.5",
"workbox-window": "^7.3.0",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+1 -1
View File
@@ -63,7 +63,7 @@ You can also create custom aliases for existing sounds using the `soundAlias` fu
<MiniRepl
client:idle
tune={`soundAlias('RolandTR808_bd', 'kick')
tune={`soundAlias("RolandTR808_bd", "kick")
s("kick")`}
/>
@@ -32,7 +32,7 @@ There are 3 quick ways to embed strudel in your website:
### Inside an iframe
Using an iframe is the most easy way to embed a strudel tune.
Using an iframe is the most easy way to embed a studel 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,11 +79,13 @@ 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, patternAutoStart } = useSettings();
const { userPatterns, patternFilter } = useSettings();
const viewingPatternID = viewingPatternData?.id;
return (
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
@@ -133,13 +135,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 }, patternAutoStart);
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
onClick={(id) =>
updateCodeWindow(
context,
{ ...userPatterns[id], collection: userPattern.collection },
autoResetPatternOnChange,
)
}
patterns={userPatterns}
started={context.started}
activePattern={activePattern}
@@ -186,14 +188,17 @@ 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 }, patternAutoStart);
updateCodeWindow(
context,
{ ...patterns[id], collection: patternFilterName.featured },
autoResetPatternOnChange,
);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetFeaturedPatterns(pageNum - 1);
@@ -208,14 +213,13 @@ 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 }, patternAutoStart);
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetPublicPatterns(pageNum - 1);
@@ -112,7 +112,6 @@ export function SettingsTab({ started }) {
multiChannelOrbits,
isTabIndentationEnabled,
isMultiCursorEnabled,
patternAutoStart,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -305,11 +304,6 @@ 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,9 +62,11 @@ export function SoundsTab() {
// stop current sound on mouseup
useEvent('mouseup', () => {
const ref = trigRef.current;
const t = trigRef.current;
trigRef.current = undefined;
ref?.stop?.(getAudioContext().currentTime + 0.01);
t?.then((ref) => {
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">
@@ -122,19 +124,12 @@ export function SoundsTab() {
duration: 0.5,
};
soundPreviewIdx++;
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect();
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);
}
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
trigRef.current.then((ref) => {
connectToDestination(ref?.node);
});
}}
>
{' '}
+5 -1
View File
@@ -92,7 +92,11 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
async function blobToDataUrl(blob) {
return new Promise((resolve) => {
resolve(URL.createObjectURL(blob));
var reader = new FileReader();
reader.onload = function (event) {
resolve(event.target.result);
};
reader.readAsDataURL(blob);
});
}
-5
View File
@@ -96,11 +96,6 @@ export function useSettings() {
isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
patternAutoStart: isUdels()
? false
: state.patternAutoStart === undefined
? true
: parseBoolean(state.patternAutoStart),
};
}