Compare commits

..

10 Commits

Author SHA1 Message Date
froos 535a822d2a Merge branch 'main' into matthewkaney/add-undocumented 2025-10-28 23:12:38 +01:00
Matthew Kaney 08cc9b03d7 Document functions in core/signal.mjs 2025-08-20 13:22:40 -04:00
Matthew Kaney 46949d165d Regenerate undocumented functions list 2025-08-19 18:39:17 -04:00
Matthew Kaney 88b94b21b8 Merge remote-tracking branch 'upstream/main' into matthewkaney/add-undocumented 2025-08-19 18:19:06 -04:00
Matthew Kaney 80d6a1903a Merge branch 'main' into add-undocumented 2025-03-24 16:53:55 -04:00
Matthew Kaney 35d98f4d40 Merge remote-tracking branch 'upstream/main' into add-undocumented 2025-01-31 16:19:20 -05:00
Matthew Kaney 2c7198a5f0 Add snapshot tests for new examples 2025-01-31 16:18:19 -05:00
Matthew Kaney 58c88c56ba Add documentation for some undocumented functions 2025-01-31 16:10:51 -05:00
Matthew Kaney ecad4d0b11 Fix issue with undocumented checker not acknowledging synonyms 2025-01-28 16:10:54 -05:00
Matthew Kaney fddb66d74c Update list of undocumented functions 2025-01-24 16:45:29 -05:00
73 changed files with 6877 additions and 7454 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"
}
}
+2 -4
View File
@@ -10,7 +10,7 @@ function getExports(code) {
let ast;
try {
ast = parse(code, {
ecmaVersion: 11,
ecmaVersion: 'latest',
sourceType: 'module',
});
} catch (err) {
@@ -49,9 +49,7 @@ function getExports(code) {
}
function isDocumented(name, docs) {
return docs.find(
(d) => d.name === name || d.tags?.find((t) => t.title === 'synonyms' && t.value.split(', ').includes(name)),
);
return docs.find((d) => d.name === name || d.synonyms?.includes(name));
}
async function getUndocumented(path, docs) {
+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"
}
}
+2
View File
@@ -3289,6 +3289,7 @@ export const striate = register('striate', function (n, pat) {
/**
* Makes the sample fit the given number of cycles by changing the speed.
* @name loopAt
* @synonyms loopat
* @memberof Pattern
* @returns Pattern
* @example
@@ -3421,6 +3422,7 @@ export const fit = register('fit', (pat) =>
* given by a global clock and this function will be
* deprecated/removed.
* @name loopAtCps
* @synonyms loopatcps
* @memberof Pattern
* @returns Pattern
* @example
+38 -8
View File
@@ -10,11 +10,27 @@ import Fraction from './fraction.mjs';
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
/**
* A `signal` consisting of a constant value. Similar to `pure`, except that function
* creates a pattern with one event per cycle, whereas this pattern doesn't have an intrinsic
* structure.
*
* @param {*} value The constant value of the resulting pattern
* @returns Pattern
*/
export function steady(value) {
// A continuous value
return new Pattern((state) => [new Hap(undefined, state.span, value)]);
}
/**
* Creates a "signal", an unstructured pattern consisting of a single value that changes
* over time.
*
*
* @param {*} func
* @returns Pattern
*/
export const signal = (func) => {
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin))];
return new Pattern(query);
@@ -23,7 +39,7 @@ export const signal = (func) => {
/**
* A sawtooth signal between 0 and 1.
*
* @return {Pattern}
* @type {Pattern}
* @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(saw.slow(2))
@@ -157,6 +173,7 @@ export const time = signal(id);
/**
* The mouse's x position value ranges from 0 to 1.
* @name mousex
* @synonyms mouseX
* @return {Pattern}
* @example
* n(mousex.segment(4).range(0,7)).scale("C:minor")
@@ -166,6 +183,7 @@ export const time = signal(id);
/**
* The mouse's y position value ranges from 0 to 1.
* @name mousey
* @synonyms mouseY
* @return {Pattern}
* @example
* n(mousey.segment(4).range(0,7)).scale("C:minor")
@@ -215,10 +233,6 @@ const timeToRandsPrime = (seed, n) => {
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/**
*
*/
/**
* A discrete pattern of numbers from 0 to n-1
* @example
@@ -389,17 +403,23 @@ export const chooseInWith = (pat, xs) => {
/**
* Chooses randomly from the given list of elements.
* @synonyms chooseOut
* @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(choose("sine", "triangle", "bd:6"))
*/
export const choose = (...xs) => chooseWith(rand, xs);
// todo: doc
export const chooseIn = (...xs) => chooseInWith(rand, xs);
export const chooseOut = choose;
/**
* As with {choose}, but the structure comes from the chosen values, rather
* than the pattern you're using to choose with.
* @param {...any} xs
* @returns
*/
export const chooseIn = (...xs) => chooseInWith(rand, xs);
/**
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
@@ -496,6 +516,7 @@ function _perlin(t) {
const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb));
return v;
}
export const perlinWith = (tpat) => {
return tpat.fmap(_perlin);
};
@@ -541,6 +562,15 @@ export const perlin = perlinWith(time.fmap((v) => Number(v)));
*/
export const berlin = berlinWith(time.fmap((v) => Number(v)));
/**
* Removes events from a pattern given a signal of numbers and a cutoff
* value for interpreting that pattern as true or false.
* @param {number} withPat - A numeric pattern for comparing the main pattern
* against. Values higher than the cutoff will let events through and lower
* values will remove events from the main pattern
* @param {number} cutoff - The threshold value to use when interpreting the
* `withPat`
*/
export const degradeByWith = register(
'degradeByWith',
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
-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"
}
-6
View File
@@ -40,12 +40,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
-4
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
-4
View File
@@ -14,10 +14,6 @@ export const buttonMap = {
rt: 7,
back: 8,
start: 9,
l3: 10,
ls: 10,
r3: 11,
rs: 11,
u: 12,
up: 12,
d: 13,
-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"
}
}
+250 -171
View File
@@ -32,7 +32,12 @@
"/packages/codemirror/keybindings.mjs": [
"keybindings"
],
"/packages/codemirror/themes/strudel-theme.mjs": [],
"/packages/codemirror/themes/theme-helper.mjs": [
"createTheme"
],
"/packages/codemirror/themes/strudel-theme.mjs": [
"settings"
],
"/packages/codemirror/themes/bluescreen.mjs": [
"settings"
],
@@ -48,7 +53,103 @@
"/packages/codemirror/themes/algoboy.mjs": [
"settings"
],
"/packages/codemirror/themes/terminal.mjs": [
"/packages/codemirror/themes/CutiePi.mjs": [
"settings"
],
"/packages/codemirror/themes/sonic-pink.mjs": [
"settings"
],
"/packages/codemirror/themes/red-text.mjs": [
"settings"
],
"/packages/codemirror/themes/green-text.mjs": [
"settings"
],
"/packages/codemirror/themes/archBtw.mjs": [
"settings"
],
"/packages/codemirror/themes/fruitDaw.mjs": [
"settings"
],
"/packages/codemirror/themes/bluescreenlight.mjs": [
"settings"
],
"/packages/codemirror/themes/androidstudio.mjs": [
"settings"
],
"/packages/codemirror/themes/atomone.mjs": [
"settings"
],
"/packages/codemirror/themes/aura.mjs": [
"settings"
],
"/packages/codemirror/themes/darcula.mjs": [
"settings"
],
"/packages/codemirror/themes/dracula.mjs": [
"settings"
],
"/packages/codemirror/themes/duotoneDark.mjs": [
"settings"
],
"/packages/codemirror/themes/eclipse.mjs": [
"settings"
],
"/packages/codemirror/themes/githubDark.mjs": [
"settings"
],
"/packages/codemirror/themes/githubLight.mjs": [
"settings"
],
"/packages/codemirror/themes/gruvboxDark.mjs": [
"settings"
],
"/packages/codemirror/themes/gruvboxLight.mjs": [
"settings"
],
"/packages/codemirror/themes/materialDark.mjs": [
"settings"
],
"/packages/codemirror/themes/materialLight.mjs": [
"settings"
],
"/packages/codemirror/themes/nord.mjs": [
"settings"
],
"/packages/codemirror/themes/monokai.mjs": [
"settings"
],
"/packages/codemirror/themes/solarizedDark.mjs": [
"settings"
],
"/packages/codemirror/themes/solarizedLight.mjs": [
"settings"
],
"/packages/codemirror/themes/sublime.mjs": [
"settings"
],
"/packages/codemirror/themes/tokyoNight.mjs": [
"settings"
],
"/packages/codemirror/themes/tokioNightStorm.mjs": [
"settings"
],
"/packages/codemirror/themes/tokyoNightDay.mjs": [
"settings"
],
"/packages/codemirror/themes/vscodeDark.mjs": [
"settings"
],
"/packages/codemirror/themes/vscodeLight.mjs": [
"settings"
],
"/packages/codemirror/themes/xcodeLight.mjs": [
"settings"
],
"/packages/codemirror/themes/bbedit.mjs": [
"settings"
],
"/packages/codemirror/themes/noctisLilac.mjs": [
"settings"
],
"/packages/codemirror/themes.mjs": [
@@ -61,7 +162,12 @@
"activateTheme"
],
"/packages/codemirror/slider.mjs": [
"acorn parse error: SyntaxError: undefined"
"sliderValues",
"SliderWidget",
"setSliderWidgets",
"updateSliderWidgets",
"sliderPlugin",
"sliderWithID"
],
"/packages/codemirror/widget.mjs": [
"addWidget",
@@ -78,6 +184,59 @@
"StrudelMirror"
],
"/packages/codemirror/index.mjs": [],
"/packages/core/logger.mjs": [
"logKey",
"errorLogger",
"logger"
],
"/packages/core/util.mjs": [
"isNoteWithOctave",
"isNote",
"tokenizeNote",
"noteToMidi",
"midiToFreq",
"freqToMidi",
"valueToMidi",
"getEventOffsetMs",
"_mod",
"averageArray",
"nanFallback",
"getSoundIndex",
"getPlayableNoteValue",
"getFrequency",
"rotate",
"pipe",
"compose",
"flatten",
"id",
"constant",
"listRange",
"curry",
"parseNumeral",
"mapArgs",
"numeralArgs",
"parseFractional",
"fractionalArgs",
"splitAt",
"zipWith",
"pairs",
"clamp",
"sol2note",
"uniq",
"uniqsort",
"uniqsortr",
"unicodeToBase64",
"base64ToUnicode",
"code2hash",
"hash2code",
"objectMap",
"cycleToSeconds",
"ClockCollator",
"getPerformanceTimeSeconds",
"keyAlias",
"getCurrentKeyboardState",
"stringifyValues"
],
"/packages/core/fraction.mjs": [
"gcd",
"lcm"
@@ -91,45 +250,6 @@
"/packages/core/state.mjs": [
"State"
],
"/packages/core/logger.mjs": [
"logKey",
"logger"
],
"/packages/core/util.mjs": [
"isNoteWithOctave",
"isNote",
"tokenizeNote",
"noteToMidi",
"midiToFreq",
"freqToMidi",
"valueToMidi",
"_mod",
"nanFallback",
"getSoundIndex",
"getPlayableNoteValue",
"getFrequency",
"rotate",
"pipe",
"compose",
"flatten",
"constant",
"listRange",
"curry",
"parseNumeral",
"mapArgs",
"numeralArgs",
"parseFractional",
"fractionalArgs",
"splitAt",
"zipWith",
"clamp",
"sol2note",
"unicodeToBase64",
"base64ToUnicode",
"code2hash",
"hash2code",
"objectMap"
],
"/packages/core/value.mjs": [
"unionWithObj",
"valued",
@@ -138,10 +258,8 @@
],
"/packages/core/drawLine.mjs": [],
"/packages/core/pattern.mjs": [
"calculateSteps",
"setStringParser",
"polyrhythm",
"pr",
"pm",
"nothing",
"isPattern",
"reify",
@@ -149,8 +267,12 @@
"stackRight",
"stackCentre",
"stackBy",
"fastcat",
"_polymeterListSteps",
"bind",
"innerBind",
"outerBind",
"squeezeBind",
"stepBind",
"polyBind",
"set",
"keep",
"keepif",
@@ -174,96 +296,51 @@
"func",
"compressSpan",
"compressspan",
"fastgap",
"focusSpan",
"focusspan",
"density",
"sparsity",
"zoomArc",
"zoomarc",
"inv",
"juxby",
"echowith",
"stutWith",
"stutwith",
"iterback",
"slowchunk",
"slowChunk",
"chunkback",
"fastchunk",
"bypass",
"hsla",
"hsl",
"loopat",
"loopatcps"
"_retime",
"_slices",
"_fitslice",
"_match",
"_polymeterListSteps",
"shrinklist",
"s_cat",
"s_alt",
"s_polymeter",
"s_taper",
"s_taperlist",
"s_add",
"s_sub",
"s_expand",
"s_extend",
"s_contract",
"s_tour",
"s_zip",
"steps",
"_morph"
],
"/packages/core/controls.mjs": [
"createParam",
"sound",
"src",
"att",
"fmi",
"isControlName",
"registerControl",
"fmrelease",
"fmvelocity",
"analyze",
"fft",
"dec",
"sus",
"rel",
"hold",
"bandf",
"bp",
"bandq",
"loopb",
"loope",
"ch",
"duck",
"phaserrate",
"phasr",
"ph",
"phs",
"phc",
"phd",
"phasdp",
"cutoff",
"ctf",
"lp",
"lpe",
"hpe",
"bpe",
"lpa",
"hpa",
"bpa",
"lpd",
"hpd",
"bpd",
"lps",
"hps",
"bps",
"lpr",
"hpr",
"bpr",
"fanchor",
"vibrato",
"v",
"vmod",
"hcutoff",
"hp",
"hresonance",
"resonance",
"delayfb",
"dfb",
"delayt",
"dt",
"lock",
"det",
"fadeTime",
"fadeOutTime",
"fadeInTime",
"patt",
"pdec",
"psustain",
"psus",
"prel",
"gate",
"gat",
"activeLabel",
@@ -291,26 +368,13 @@
"offset",
"octaves",
"mode",
"rlp",
"rdim",
"rfade",
"ir",
"size",
"sz",
"rsize",
"dist",
"compressorKnee",
"compressorRatio",
"compressorAttack",
"compressorRelease",
"waveloss",
"density",
"expression",
"sustainpedal",
"tremolodepth",
"tremdp",
"tremolorate",
"tremr",
"fshift",
"fshiftnote",
"fshiftphase",
@@ -336,27 +400,15 @@
"binshift",
"hbrick",
"lbrick",
"midichan",
"control",
"ccn",
"ccv",
"polyTouch",
"midibend",
"miditouch",
"ctlNum",
"frameRate",
"frames",
"hours",
"midicmd",
"minutes",
"progNum",
"seconds",
"songPtr",
"uid",
"val",
"cps",
"legato",
"dur",
"zrand",
"curve",
"deltaSlide",
@@ -368,44 +420,40 @@
"zmod",
"zcrush",
"zdelay",
"tremolo",
"zzfx",
"colour",
"createParams",
"ad",
"ds",
"ar"
"ar",
"midimap",
"ctlNum",
"polyTouch",
"getControlName"
],
"/packages/core/euclid.mjs": [
"bjork",
"e",
"euclidrot"
],
"/packages/core/zyklus.mjs": [],
"/packages/core/signal.mjs": [
"steady",
"signal",
"isaw",
"isaw2",
"saw2",
"sine2",
"cosine2",
"square2",
"tri2",
"time",
"randrun",
"_brandBy",
"_irand",
"pickSqueeze",
"pickmodSqueeze",
"__chooseWith",
"randcat",
"wrandcat",
"chooseIn",
"chooseOut",
"perlinWith",
"degradeByWith"
"berlinWith",
"degradeByWith",
"_keyDown"
],
"/packages/core/pick.mjs": [],
"/packages/core/speak.mjs": [
"speak"
],
"/packages/core/evaluate.mjs": [
"strudelScope",
"evalScope",
"evaluate"
],
@@ -440,13 +488,18 @@
"isTauri"
],
"/packages/desktopbridge/midibridge.mjs": [],
"/packages/desktopbridge/oscbridge.mjs": [],
"/packages/desktopbridge/oscbridge.mjs": [
"oscTriggerTauri"
],
"/packages/desktopbridge/index.mjs": [],
"/packages/draw/draw.mjs": [
"getDrawContext",
"cleanupDraw",
"Framer",
"Drawer"
"Drawer",
"getComputedPropertyValue",
"getTheme",
"setTheme"
],
"/packages/draw/animate.mjs": [
"x",
@@ -467,16 +520,19 @@
"convertHexToNumber"
],
"/packages/draw/pianoroll.mjs": [
"__pianoroll",
"getDrawOptions",
"getPunchcardPainter",
"drawPianoroll"
],
"/packages/draw/spiral.mjs": [],
"/packages/draw/pitchwheel.mjs": [],
"/packages/draw/index.mjs": [],
"/packages/midi/midi.mjs": [
"WebMidi",
"enableWebMidi",
"midin"
"midicontrolMap",
"midisoundMap"
],
"/packages/midi/index.mjs": [],
"/packages/mini/krill-parser.js": [],
@@ -496,12 +552,11 @@
"/packages/repl/prebake.mjs": [
"prebake"
],
"/packages/repl/repl-component.mjs": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/repl/repl-component.mjs": [],
"/packages/repl/index.mjs": [],
"/packages/soundfonts/gm.mjs": [],
"/packages/soundfonts/fontloader.mjs": [
"setSoundfontUrl",
"getFontBufferSource",
"getFontPitch",
"registerSoundfonts"
@@ -522,6 +577,7 @@
"vowelFormant"
],
"/packages/superdough/logger.mjs": [
"errorLogger",
"logger",
"setLogger"
],
@@ -534,10 +590,19 @@
"valueToMidi",
"nanFallback",
"_mod",
"getSoundIndex"
"getSoundIndex",
"cycleToSeconds",
"secondsToCycle"
],
"/packages/superdough/noise.mjs": [
"getNoiseBuffer",
"getNoiseOscillator",
"getNoiseMix"
],
"/packages/superdough/helpers.mjs": [
"noises",
"gainNode",
"getWorklet",
"getParamADSR",
"getCompressor",
"getADSRValues",
@@ -550,6 +615,8 @@
],
"/packages/superdough/sampler.mjs": [
"getCachedBuffer",
"getSampleInfo",
"getSampleBuffer",
"getSampleBufferSource",
"loadBuffer",
"reverseBuffer",
@@ -559,18 +626,32 @@
"onTriggerSample"
],
"/packages/superdough/superdough.mjs": [
"DEFAULT_MAX_POLYPHONY",
"setMaxPolyphony",
"setMultiChannelOrbits",
"soundMap",
"registerSound",
"applyGainCurve",
"setGainCurve",
"getSound",
"getAudioDevices",
"setDefault",
"resetDefaults",
"setDefaultValue",
"getDefaultValue",
"setDefaultValues",
"resetDefaultValues",
"setVersionDefaults",
"resetLoadedSounds",
"setDefaultAudioContext",
"getAudioContext",
"getWorklet",
"getAudioContextCurrentTime",
"initAudio",
"initAudioOnFirstClick",
"initializeAudioOutput",
"connectToDestination",
"panic",
"getLfo",
"analysers",
"analysersData",
"getAnalyserById",
@@ -579,17 +660,13 @@
"superdough",
"superdoughTrigger"
],
"/packages/superdough/noise.mjs": [
"getNoiseOscillator",
"getNoiseMix"
],
"/packages/superdough/synth.mjs": [
"registerSynthSounds",
"waveformN",
"getOscillator"
],
"/packages/superdough/zzfx_fork.mjs": [
"acorn parse error: SyntaxError: undefined"
"buildSamples"
],
"/packages/superdough/zzfx.mjs": [
"getZZFX",
@@ -640,6 +717,7 @@
],
"/packages/transpiler/transpiler.mjs": [
"registerWidgetType",
"registerLanguage",
"transpiler",
"getWidgetID"
],
@@ -654,6 +732,7 @@
"drawTimeScope",
"drawFrequencyScope"
],
"/packages/webaudio/spectrum.mjs": [],
"/packages/webaudio/index.mjs": [],
"/packages/xen/xen.mjs": [
"edo",
-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),
};
}