Compare commits

..

42 Commits

Author SHA1 Message Date
Felix Roos 941c97da0d fix: cache bunny cdn urls #2057 2026-06-15 09:15:06 +02:00
Alex McLean 95a9d301a1 Merge pull request 'midikeys-with-channel' (#2063) from eefano/strudel:midikeys-with-channel into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2063
2026-06-10 13:38:02 +02:00
eefano 2635716c87 added midikeys third test 2026-06-02 14:56:55 +02:00
eefano 58f956b57e prettier fix 2026-06-02 14:29:28 +02:00
eefano 0ae2c120b3 updated documentation for midikeys 2026-06-02 13:28:23 +02:00
eefano 05a43ef687 included midichan value in midikeys haps 2026-06-02 13:14:30 +02:00
Alex McLean 0c61cd7670 Merge pull request 'Don't force SSL for mqtt websocket connections' (#2058) from mqtt-ws into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2058
2026-05-27 13:35:28 +02:00
alex 13b11efa15 don't force SSL for mqtt websocket connections - fall back on url instead (wss: for SSL, ws: for unencrypted) 2026-05-26 16:11:33 +01:00
froos f73b395648 Merge pull request 'Add error for when name arg in register is a pattern' (#2020) from tyow/localstrudel:registerErrorFix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2020
2026-05-07 11:46:59 +02:00
froos 9c47efd3cf Merge pull request 'docs: note that all/each require labeled patterns' (#2015) from bwagner/strudel:fix/all-unlabeled-pattern-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2015
2026-05-07 11:28:31 +02:00
froos f4066089f4 Merge pull request 'Add flux function' (#1528) from TodePond/strudel:lu/flux into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1528
2026-05-07 10:51:57 +02:00
Felix Roos 0e81987c4e Merge remote-tracking branch 'origin/main' into lu/flux 2026-05-07 10:36:11 +02:00
Felix Roos 23dbe53013 add snapshots 2026-05-07 10:32:58 +02:00
froos 0e4d0484a3 Merge pull request 'Fix the documentation of stretch' (#1670) from vvolhejn/strudel:vv/fix-stretch-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1670
2026-05-07 09:32:37 +02:00
froos 37bf2e4dc7 Merge pull request 'Added .dockerignore' (#2005) from KuboOrk/strudel:docker/add-dockerignore into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2005
2026-05-07 00:45:30 +02:00
froos e69761397e Merge pull request 'fix midi numbers as notes for osc backend' (#2001) from jagen31/strudel:jagen-midinote-osc into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2001
2026-05-07 00:20:41 +02:00
froos 10c33f24f5 Merge pull request 'LFO documentation' (#2000) from scrappy_fiddler/strudel:lfo_docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2000
2026-05-07 00:11:54 +02:00
Felix Roos ea0ec7edb9 fix format 2026-05-06 22:14:11 +02:00
froos f900642359 cross-reference signals page 2026-05-06 22:01:58 +02:00
froos 4cc63b8b01 Merge pull request 'fixed bug where withBase would silence a pattern if freq was unset' (#1994) from tyow/localstrudel:withbase-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1994
2026-05-06 21:29:34 +02:00
tyow ae32743f12 format 2026-03-13 14:11:38 -04:00
tyow 7294cd5443 Add error for when name arg in register is a pattern 2026-03-13 14:11:19 -04:00
Bernhard Wagner cdae661b24 docs: note that all/each require labeled patterns 2026-03-09 11:15:14 +01:00
TodePond 63f42faca8 Update packages/core/pattern.mjs 2026-03-03 11:27:05 +01:00
Václav Volhejn 2624b97500 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into vv/fix-stretch-docs 2026-03-01 11:15:28 +01:00
Václav Volhejn 633b7287c0 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into vv/fix-stretch-docs 2026-03-01 11:13:14 +01:00
KuboOrk 3805ba8f76 Added .dockerignore 2026-02-28 13:39:07 +03:00
jagen31 82e88d4a28 fix midi numbers as notes for osc backend
note -> midinote
2026-02-23 01:24:53 +01:00
scrappy_fiddler 5cd45d5ea1 typo in lfo docs 2026-02-23 00:22:30 +01:00
scrappy_fiddler d930057000 lfo documentation 2026-02-23 00:18:42 +01:00
scrappy_fiddler daf27ed971 added lfo documentation 2026-02-23 00:11:17 +01:00
tyow 8520044f3e fixed bug where withBase would silence a pattern if freq was unset 2026-02-20 13:10:16 -05:00
Václav Volhejn 7c22b29493 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into vv/fix-stretch-docs 2026-01-10 06:34:34 -08:00
Lu[ke] Wilson c77675c492 add another synonym 2025-11-29 18:01:27 +00:00
Lu[ke] Wilson 5197af1aa4 Merge remote-tracking branch 'origin/main' into lu/flux 2025-11-29 17:58:41 +00:00
froos 85f6472894 Merge branch 'main' into lu/flux 2025-10-28 23:53:44 +01:00
Václav Volhejn 54fd877be1 Update snapshot 2025-10-18 13:19:18 +02:00
Václav Volhejn 7d948fd1a7 Fix example snapshot 2025-10-18 12:25:15 +02:00
Václav Volhejn 3373fa39a6 Fix the documentation of stretch 2025-10-18 12:14:39 +02:00
froos 1de8123437 Merge branch 'main' into lu/flux 2025-09-14 01:12:28 +02:00
Lu[ke] Wilson 781c477078 rename to juxFlip 2025-08-29 17:20:18 +01:00
Lu[ke] Wilson e2cdce5ac9 add flux function 2025-08-29 15:35:41 +01:00
13 changed files with 624 additions and 36 deletions
+11
View File
@@ -0,0 +1,11 @@
**/node_modules
**/.angular
**/dist
.astro/
.forgejo/
.idea/
.vscode/
test/
.gitignore
Dockerfile
.dockerignore
-17
View File
@@ -1,17 +0,0 @@
This place is a message... and part of a system of messages... pay attention to it!
Sending this message was important to us. We considered ourselves to be a powerful culture.
This place is not a place of honor... no highly esteemed deed is commemorated here... nothing valued is here.
What is here was dangerous and repulsive to us. This message is a warning about danger.
The danger is in a particular location... it increases towards a center... the center of danger is here... of a particular size and shape, and below us.
The danger is still present, in your time, as it was in ours.
The danger is to the body, and it can kill.
The form of the danger is an emanation of energy.
The danger is unleashed only if you substantially disturb this place physically. This place is best shunned and left uninhabited.
+7 -3
View File
@@ -2582,13 +2582,17 @@ export const { compressorRelease } = registerControl('compressorRelease');
export const { speed } = registerControl('speed');
/**
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
* Changes the pitch of the sample without changing its speed.
* The frequencies are multiplied by (factor + 1) for positive numbers
* and by max(factor / 4 + 1, 0) for negative numbers.
* So tuning up by octaves can be done with 1, 3, 7, ...
* and tuning down by octaves with -2, -3, -3.5...
*
* @name stretch
* @tags pitch, samples
* @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards.
* @param {number | Pattern} factor between `-4` and `inf`. Positive increases pitch, 0 does nothing, negative decreases the pitch.
* @example
* s("gm_flute").stretch("1 2 .5")
* s("gm_flute").stretch("<2 1 0 -2>")
*
*/
export const { stretch } = registerControl('stretch');
+35
View File
@@ -1741,6 +1741,12 @@ export const func = curry((a, b) => reify(b).func(a));
*
*/
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
if (isPattern(name)) {
throw new Error(
'Name argument for register is a pattern, try using single quotes (\'name\') instead of double quotes ("name")',
);
}
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
@@ -2546,6 +2552,20 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
return stack(left, right).setSteps(__steps ? lcm(left._steps, right._steps) : undefined);
});
/**
* Like juxBy, except it flips the ears each cycle.
* @name juxFlipBy
* @synonyms juxflipby, fluxBy, fluxby
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlipBy(".8", rev)
*/
export const { juxFlipBy, juxflipby, fluxBy, fluxby } = register(
['juxFlipBy', 'juxflipby', 'fluxBy', 'fluxby'],
function (by, func, pat) {
return pat.juxBy(slowcat(by, -by), func);
},
);
/**
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
* @tags temporal, superdough
@@ -2560,6 +2580,21 @@ export const jux = register('jux', function (func, pat) {
return pat._juxBy(1, func, pat);
});
/**
* Like jux, but flips the ears each cycle.
* @name juxFlip
* @synonyms juxflip, flux
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(rev)
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(press)
* @example
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(iter(4))
*/
export const { juxFlip, flux } = register(['juxFlip', 'juxflip', 'flux'], function (func, pat) {
return pat._juxFlipBy(1, func, pat);
});
/**
* Superimpose and offset multiple times, applying the given function each time.
* @tags temporal, functional
+8 -1
View File
@@ -307,8 +307,12 @@ export function repl({
let allTransforms = [];
/**
* Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
* Applies a function to all the running patterns. Note that the patterns are grouped together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
* a version that applies the function to each pattern separately.
*
* **Note:** Patterns must be labeled (e.g. with `$:`) to be picked up by `all`. An unlabeled
* pattern such as `note("c4")` is not registered and will produce no audio when `all` is present.
* Use `$: note("c4")` instead.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
@@ -328,6 +332,9 @@ export function repl({
};
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
*
* **Note:** Patterns must be labeled (e.g. with `$:`) to be picked up by `each`. An unlabeled
* pattern such as `note("c4")` is not registered and will produce no audio when `each` is present.
* Use `$: note("c4")` instead.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
+8 -1
View File
@@ -537,6 +537,9 @@ export async function midin(input) {
* The note length is fixed as Superdough is not currently set up for undetermined
* note durations
*
* The 'midichan' control value contains the number of the channel the note is coming from
* so it could be filtered or manipulated further in the chain.
*
* @name midikeys
* @tags external_io, midi
* @param {string | number} input MIDI device name or index defaulting to 0
@@ -552,6 +555,10 @@ export async function midin(input) {
* .s("saw")
* .add(note(rand.mul(0.3)))
* .lpf(1000).lpe(2).room(0.5)
* @example
* // discard all notes not coming out from midi channel 2
* const kb = await midikeys('Arturia KeyStep 32')
* kb().filterValues(v=>v.midichan==2).s("tri")
*/
const kHaps = {};
const kListeners = {};
@@ -633,7 +640,7 @@ export async function midikeys(input) {
*/
return;
} else {
value = { ...value, note: Math.round(note), velocity: velocity / 127 };
value = { ...value, note: Math.round(note), velocity: velocity / 127, midichan: message.channel };
}
kHaps[input].push(new Hap(span, span, value, {}));
if (!noteoff && triggerAvailable) {
-1
View File
@@ -61,7 +61,6 @@ Pattern.prototype.mqtt = function (
const props = {
onSuccess: onConnect,
onFailure: onFailure,
useSSL: true,
};
if (username) {
+1 -1
View File
@@ -42,7 +42,7 @@ export function parseControlsFromHap(hap, cps) {
if (isNote(controls.note)) {
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
} else {
controls.note = parseNumeral(controls.note);
controls.midinote = parseNumeral(controls.note);
}
}
controls.bank && (controls.s = controls.bank + controls.s);
+1
View File
@@ -149,6 +149,7 @@ export const withBase = register('withBase', (b, pat) => {
let hVal = hap.value;
const isObject = typeof hVal === 'object';
let freq = isObject ? hVal.freq : hVal;
if (!freq) return hap;
freq = (freq * base) / originalBase;
hap.value = isObject ? { ...hap.value, freq } : { freq };
return hap;
+250 -12
View File
@@ -6527,6 +6527,250 @@ exports[`runs examples > example "juxBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "juxFlip" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd pan:0 ]",
"[ 0/1 → 1/8 | s:hh pan:1 ]",
"[ 1/8 → 1/4 | s:lt pan:0 ]",
"[ 1/8 → 1/4 | s:bd pan:1 ]",
"[ 5/16 → 3/8 | s:ht pan:0 ]",
"[ 3/8 → 1/2 | s:mt pan:0 ]",
"[ 3/8 → 1/2 | s:cp pan:1 ]",
"[ 1/2 → 5/8 | s:cp pan:0 ]",
"[ 1/2 → 5/8 | s:mt pan:1 ]",
"[ 5/8 → 11/16 | s:ht pan:1 ]",
"[ 3/4 → 7/8 | s:bd pan:0 ]",
"[ 3/4 → 7/8 | s:lt pan:1 ]",
"[ 7/8 → 1/1 | s:hh pan:0 ]",
"[ 7/8 → 1/1 | s:bd pan:1 ]",
"[ 1/1 → 9/8 | s:bd pan:1 ]",
"[ 1/1 → 9/8 | s:hh pan:0 ]",
"[ 9/8 → 5/4 | s:lt pan:1 ]",
"[ 9/8 → 5/4 | s:bd pan:0 ]",
"[ 21/16 → 11/8 | s:ht pan:1 ]",
"[ 11/8 → 3/2 | s:mt pan:1 ]",
"[ 11/8 → 3/2 | s:cp pan:0 ]",
"[ 3/2 → 13/8 | s:cp pan:1 ]",
"[ 3/2 → 13/8 | s:mt pan:0 ]",
"[ 13/8 → 27/16 | s:ht pan:0 ]",
"[ 7/4 → 15/8 | s:bd pan:1 ]",
"[ 7/4 → 15/8 | s:lt pan:0 ]",
"[ 15/8 → 2/1 | s:hh pan:1 ]",
"[ 15/8 → 2/1 | s:bd pan:0 ]",
"[ 2/1 → 17/8 | s:bd pan:0 ]",
"[ 2/1 → 17/8 | s:hh pan:1 ]",
"[ 17/8 → 9/4 | s:lt pan:0 ]",
"[ 17/8 → 9/4 | s:bd pan:1 ]",
"[ 37/16 → 19/8 | s:ht pan:0 ]",
"[ 19/8 → 5/2 | s:mt pan:0 ]",
"[ 19/8 → 5/2 | s:cp pan:1 ]",
"[ 5/2 → 21/8 | s:cp pan:0 ]",
"[ 5/2 → 21/8 | s:mt pan:1 ]",
"[ 21/8 → 43/16 | s:ht pan:1 ]",
"[ 11/4 → 23/8 | s:bd pan:0 ]",
"[ 11/4 → 23/8 | s:lt pan:1 ]",
"[ 23/8 → 3/1 | s:hh pan:0 ]",
"[ 23/8 → 3/1 | s:bd pan:1 ]",
"[ 3/1 → 25/8 | s:bd pan:1 ]",
"[ 3/1 → 25/8 | s:hh pan:0 ]",
"[ 25/8 → 13/4 | s:lt pan:1 ]",
"[ 25/8 → 13/4 | s:bd pan:0 ]",
"[ 53/16 → 27/8 | s:ht pan:1 ]",
"[ 27/8 → 7/2 | s:mt pan:1 ]",
"[ 27/8 → 7/2 | s:cp pan:0 ]",
"[ 7/2 → 29/8 | s:cp pan:1 ]",
"[ 7/2 → 29/8 | s:mt pan:0 ]",
"[ 29/8 → 59/16 | s:ht pan:0 ]",
"[ 15/4 → 31/8 | s:bd pan:1 ]",
"[ 15/4 → 31/8 | s:lt pan:0 ]",
"[ 31/8 → 4/1 | s:hh pan:1 ]",
"[ 31/8 → 4/1 | s:bd pan:0 ]",
]
`;
exports[`runs examples > example "juxFlip" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:bd pan:0 ]",
"[ 1/16 → 1/8 | s:bd pan:1 ]",
"[ 1/8 → 1/4 | s:lt pan:0 ]",
"[ 3/16 → 1/4 | s:lt pan:1 ]",
"[ 5/16 → 3/8 | s:ht pan:0 ]",
"[ 11/32 → 3/8 | s:ht pan:1 ]",
"[ 3/8 → 1/2 | s:mt pan:0 ]",
"[ 7/16 → 1/2 | s:mt pan:1 ]",
"[ 1/2 → 5/8 | s:cp pan:0 ]",
"[ 9/16 → 5/8 | s:cp pan:1 ]",
"[ 3/4 → 7/8 | s:bd pan:0 ]",
"[ 13/16 → 7/8 | s:bd pan:1 ]",
"[ 7/8 → 1/1 | s:hh pan:0 ]",
"[ 15/16 → 1/1 | s:hh pan:1 ]",
"[ 1/1 → 9/8 | s:bd pan:1 ]",
"[ 17/16 → 9/8 | s:bd pan:0 ]",
"[ 9/8 → 5/4 | s:lt pan:1 ]",
"[ 19/16 → 5/4 | s:lt pan:0 ]",
"[ 21/16 → 11/8 | s:ht pan:1 ]",
"[ 43/32 → 11/8 | s:ht pan:0 ]",
"[ 11/8 → 3/2 | s:mt pan:1 ]",
"[ 23/16 → 3/2 | s:mt pan:0 ]",
"[ 3/2 → 13/8 | s:cp pan:1 ]",
"[ 25/16 → 13/8 | s:cp pan:0 ]",
"[ 7/4 → 15/8 | s:bd pan:1 ]",
"[ 29/16 → 15/8 | s:bd pan:0 ]",
"[ 15/8 → 2/1 | s:hh pan:1 ]",
"[ 31/16 → 2/1 | s:hh pan:0 ]",
"[ 2/1 → 17/8 | s:bd pan:0 ]",
"[ 33/16 → 17/8 | s:bd pan:1 ]",
"[ 17/8 → 9/4 | s:lt pan:0 ]",
"[ 35/16 → 9/4 | s:lt pan:1 ]",
"[ 37/16 → 19/8 | s:ht pan:0 ]",
"[ 75/32 → 19/8 | s:ht pan:1 ]",
"[ 19/8 → 5/2 | s:mt pan:0 ]",
"[ 39/16 → 5/2 | s:mt pan:1 ]",
"[ 5/2 → 21/8 | s:cp pan:0 ]",
"[ 41/16 → 21/8 | s:cp pan:1 ]",
"[ 11/4 → 23/8 | s:bd pan:0 ]",
"[ 45/16 → 23/8 | s:bd pan:1 ]",
"[ 23/8 → 3/1 | s:hh pan:0 ]",
"[ 47/16 → 3/1 | s:hh pan:1 ]",
"[ 3/1 → 25/8 | s:bd pan:1 ]",
"[ 49/16 → 25/8 | s:bd pan:0 ]",
"[ 25/8 → 13/4 | s:lt pan:1 ]",
"[ 51/16 → 13/4 | s:lt pan:0 ]",
"[ 53/16 → 27/8 | s:ht pan:1 ]",
"[ 107/32 → 27/8 | s:ht pan:0 ]",
"[ 27/8 → 7/2 | s:mt pan:1 ]",
"[ 55/16 → 7/2 | s:mt pan:0 ]",
"[ 7/2 → 29/8 | s:cp pan:1 ]",
"[ 57/16 → 29/8 | s:cp pan:0 ]",
"[ 15/4 → 31/8 | s:bd pan:1 ]",
"[ 61/16 → 31/8 | s:bd pan:0 ]",
"[ 31/8 → 4/1 | s:hh pan:1 ]",
"[ 63/16 → 4/1 | s:hh pan:0 ]",
]
`;
exports[`runs examples > example "juxFlip" example index 2 1`] = `
[
"[ 0/1 → 1/8 | s:bd pan:0 ]",
"[ 0/1 → 1/8 | s:bd pan:1 ]",
"[ 1/8 → 1/4 | s:lt pan:0 ]",
"[ 1/8 → 1/4 | s:lt pan:1 ]",
"[ 5/16 → 3/8 | s:ht pan:0 ]",
"[ 5/16 → 3/8 | s:ht pan:1 ]",
"[ 3/8 → 1/2 | s:mt pan:0 ]",
"[ 3/8 → 1/2 | s:mt pan:1 ]",
"[ 1/2 → 5/8 | s:cp pan:0 ]",
"[ 1/2 → 5/8 | s:cp pan:1 ]",
"[ 3/4 → 7/8 | s:bd pan:0 ]",
"[ 3/4 → 7/8 | s:bd pan:1 ]",
"[ 7/8 → 1/1 | s:hh pan:0 ]",
"[ 7/8 → 1/1 | s:hh pan:1 ]",
"[ 1/1 → 9/8 | s:bd pan:1 ]",
"[ 17/16 → 9/8 | s:ht pan:0 ]",
"[ 9/8 → 5/4 | s:lt pan:1 ]",
"[ 9/8 → 5/4 | s:mt pan:0 ]",
"[ 5/4 → 11/8 | s:cp pan:0 ]",
"[ 21/16 → 11/8 | s:ht pan:1 ]",
"[ 11/8 → 3/2 | s:mt pan:1 ]",
"[ 3/2 → 13/8 | s:cp pan:1 ]",
"[ 3/2 → 13/8 | s:bd pan:0 ]",
"[ 13/8 → 7/4 | s:hh pan:0 ]",
"[ 7/4 → 15/8 | s:bd pan:1 ]",
"[ 7/4 → 15/8 | s:bd pan:0 ]",
"[ 15/8 → 2/1 | s:hh pan:1 ]",
"[ 15/8 → 2/1 | s:lt pan:0 ]",
"[ 2/1 → 17/8 | s:bd pan:0 ]",
"[ 2/1 → 17/8 | s:cp pan:1 ]",
"[ 17/8 → 9/4 | s:lt pan:0 ]",
"[ 9/4 → 19/8 | s:bd pan:1 ]",
"[ 37/16 → 19/8 | s:ht pan:0 ]",
"[ 19/8 → 5/2 | s:mt pan:0 ]",
"[ 19/8 → 5/2 | s:hh pan:1 ]",
"[ 5/2 → 21/8 | s:cp pan:0 ]",
"[ 5/2 → 21/8 | s:bd pan:1 ]",
"[ 21/8 → 11/4 | s:lt pan:1 ]",
"[ 11/4 → 23/8 | s:bd pan:0 ]",
"[ 45/16 → 23/8 | s:ht pan:1 ]",
"[ 23/8 → 3/1 | s:hh pan:0 ]",
"[ 23/8 → 3/1 | s:mt pan:1 ]",
"[ 3/1 → 25/8 | s:bd pan:1 ]",
"[ 3/1 → 25/8 | s:bd pan:0 ]",
"[ 25/8 → 13/4 | s:lt pan:1 ]",
"[ 25/8 → 13/4 | s:hh pan:0 ]",
"[ 13/4 → 27/8 | s:bd pan:0 ]",
"[ 53/16 → 27/8 | s:ht pan:1 ]",
"[ 27/8 → 7/2 | s:mt pan:1 ]",
"[ 27/8 → 7/2 | s:lt pan:0 ]",
"[ 7/2 → 29/8 | s:cp pan:1 ]",
"[ 57/16 → 29/8 | s:ht pan:0 ]",
"[ 29/8 → 15/4 | s:mt pan:0 ]",
"[ 15/4 → 31/8 | s:bd pan:1 ]",
"[ 15/4 → 31/8 | s:cp pan:0 ]",
"[ 31/8 → 4/1 | s:hh pan:1 ]",
]
`;
exports[`runs examples > example "juxFlipBy" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd pan:0.09999999999999998 ]",
"[ 0/1 → 1/8 | s:hh pan:0.9 ]",
"[ 1/8 → 1/4 | s:lt pan:0.09999999999999998 ]",
"[ 1/8 → 1/4 | s:bd pan:0.9 ]",
"[ 5/16 → 3/8 | s:ht pan:0.09999999999999998 ]",
"[ 3/8 → 1/2 | s:mt pan:0.09999999999999998 ]",
"[ 3/8 → 1/2 | s:cp pan:0.9 ]",
"[ 1/2 → 5/8 | s:cp pan:0.09999999999999998 ]",
"[ 1/2 → 5/8 | s:mt pan:0.9 ]",
"[ 5/8 → 11/16 | s:ht pan:0.9 ]",
"[ 3/4 → 7/8 | s:bd pan:0.09999999999999998 ]",
"[ 3/4 → 7/8 | s:lt pan:0.9 ]",
"[ 7/8 → 1/1 | s:hh pan:0.09999999999999998 ]",
"[ 7/8 → 1/1 | s:bd pan:0.9 ]",
"[ 1/1 → 9/8 | s:bd pan:0.9 ]",
"[ 1/1 → 9/8 | s:hh pan:0.09999999999999998 ]",
"[ 9/8 → 5/4 | s:lt pan:0.9 ]",
"[ 9/8 → 5/4 | s:bd pan:0.09999999999999998 ]",
"[ 21/16 → 11/8 | s:ht pan:0.9 ]",
"[ 11/8 → 3/2 | s:mt pan:0.9 ]",
"[ 11/8 → 3/2 | s:cp pan:0.09999999999999998 ]",
"[ 3/2 → 13/8 | s:cp pan:0.9 ]",
"[ 3/2 → 13/8 | s:mt pan:0.09999999999999998 ]",
"[ 13/8 → 27/16 | s:ht pan:0.09999999999999998 ]",
"[ 7/4 → 15/8 | s:bd pan:0.9 ]",
"[ 7/4 → 15/8 | s:lt pan:0.09999999999999998 ]",
"[ 15/8 → 2/1 | s:hh pan:0.9 ]",
"[ 15/8 → 2/1 | s:bd pan:0.09999999999999998 ]",
"[ 2/1 → 17/8 | s:bd pan:0.09999999999999998 ]",
"[ 2/1 → 17/8 | s:hh pan:0.9 ]",
"[ 17/8 → 9/4 | s:lt pan:0.09999999999999998 ]",
"[ 17/8 → 9/4 | s:bd pan:0.9 ]",
"[ 37/16 → 19/8 | s:ht pan:0.09999999999999998 ]",
"[ 19/8 → 5/2 | s:mt pan:0.09999999999999998 ]",
"[ 19/8 → 5/2 | s:cp pan:0.9 ]",
"[ 5/2 → 21/8 | s:cp pan:0.09999999999999998 ]",
"[ 5/2 → 21/8 | s:mt pan:0.9 ]",
"[ 21/8 → 43/16 | s:ht pan:0.9 ]",
"[ 11/4 → 23/8 | s:bd pan:0.09999999999999998 ]",
"[ 11/4 → 23/8 | s:lt pan:0.9 ]",
"[ 23/8 → 3/1 | s:hh pan:0.09999999999999998 ]",
"[ 23/8 → 3/1 | s:bd pan:0.9 ]",
"[ 3/1 → 25/8 | s:bd pan:0.9 ]",
"[ 3/1 → 25/8 | s:hh pan:0.09999999999999998 ]",
"[ 25/8 → 13/4 | s:lt pan:0.9 ]",
"[ 25/8 → 13/4 | s:bd pan:0.09999999999999998 ]",
"[ 53/16 → 27/8 | s:ht pan:0.9 ]",
"[ 27/8 → 7/2 | s:mt pan:0.9 ]",
"[ 27/8 → 7/2 | s:cp pan:0.09999999999999998 ]",
"[ 7/2 → 29/8 | s:cp pan:0.9 ]",
"[ 7/2 → 29/8 | s:mt pan:0.09999999999999998 ]",
"[ 29/8 → 59/16 | s:ht pan:0.09999999999999998 ]",
"[ 15/4 → 31/8 | s:bd pan:0.9 ]",
"[ 15/4 → 31/8 | s:lt pan:0.09999999999999998 ]",
"[ 31/8 → 4/1 | s:hh pan:0.9 ]",
"[ 31/8 → 4/1 | s:bd pan:0.09999999999999998 ]",
]
`;
exports[`runs examples > example "keep" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano ]",
@@ -7683,6 +7927,8 @@ exports[`runs examples > example "midikeys" example index 0 1`] = `[]`;
exports[`runs examples > example "midikeys" example index 1 1`] = `[]`;
exports[`runs examples > example "midikeys" example index 2 1`] = `[]`;
exports[`runs examples > example "midin" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
@@ -12404,18 +12650,10 @@ exports[`runs examples > example "stepcat" example index 1 1`] = `
exports[`runs examples > example "stretch" example index 0 1`] = `
[
"[ (0/1 → 1/3) ⇝ 1/1 | s:gm_flute stretch:1 ]",
"[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | s:gm_flute stretch:2 ]",
"[ 0/1 ⇜ (2/31/1) | s:gm_flute stretch:0.5 ]",
"[ (1/1 → 4/3) ⇝ 2/1 | s:gm_flute stretch:1 ]",
"[ 1/1 ⇜ (4/3 → 5/3) ⇝ 2/1 | s:gm_flute stretch:2 ]",
"[ 1/1 ⇜ (5/3 → 2/1) | s:gm_flute stretch:0.5 ]",
"[ (2/1 → 7/3) ⇝ 3/1 | s:gm_flute stretch:1 ]",
"[ 2/1 ⇜ (7/3 → 8/3) ⇝ 3/1 | s:gm_flute stretch:2 ]",
"[ 2/1 ⇜ (8/3 → 3/1) | s:gm_flute stretch:0.5 ]",
"[ (3/1 → 10/3) ⇝ 4/1 | s:gm_flute stretch:1 ]",
"[ 3/1 ⇜ (10/3 → 11/3) ⇝ 4/1 | s:gm_flute stretch:2 ]",
"[ 3/1 ⇜ (11/3 → 4/1) | s:gm_flute stretch:0.5 ]",
"[ 0/1 → 1/1 | s:gm_flute stretch:2 ]",
"[ 1/1 → 2/1 | s:gm_flute stretch:1 ]",
"[ 2/13/1 | s:gm_flute stretch:0 ]",
"[ 3/1 → 4/1 | s:gm_flute stretch:-2 ]",
]
`;
+1
View File
@@ -78,6 +78,7 @@ export default defineConfig({
urlPattern: ({ url }) =>
[
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
/^https:\/\/strudel\.b-cdn\.net\/.*/i,
/^https:\/\/freesound\.org\/.*/i,
/^https:\/\/cdn\.freesound\.org\/.*/i,
/^https:\/\/shabda\.ndre\.gr\/.*/i,
+1
View File
@@ -98,6 +98,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'Random Modifiers', link: 'learn/random-modifiers' },
{ text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' },
{ text: 'Accumulation', link: 'learn/accumulation' },
{ text: 'LFOs', link: 'learn/lfo' },
{ text: 'Tonal Functions', link: 'learn/tonal' },
{ text: 'Stepwise Functions', link: 'learn/stepwise' },
],
+301
View File
@@ -0,0 +1,301 @@
---
title: Low-frequency oscillators (LFO)
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Low frequency oscillators (LFO)
A low frequency oscillator (or short LFO) is a common way on synthesizers to continuously modulate various signals.
This documentation is an interactive version of glossing's tutorial https://www.youtube.com/watch?v=11frBA9L638
## Signals vs LFOs
In Strudel, there are two ways to modulate:
1. [signals](/learn/signals/) for pattern-level modulation
2. `lfo` (this page) for audio-level modulation
## Applying an LFO
Here, the LFO will change the frequency of the `saw`. Put a comment like this `//.lfo()` to see and hear how it changes, and remove the comment again.
<MiniRepl
client:idle
tune={`
s("saw").lfo()
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
By default, the LFO will modulate the control parameter which is right before `.lfo()`:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800).lfo()
._spectrum({height: 300, width: 800})`}
/>
Here, the LFO will modulate the low pass filter `.lpf`.
## Moving away from the default
The following sections explain how pass parameters to `.lfo`. Similar to `._spectrum` above, almost all the configuration of `lfo` lives inside a json object, starting with `{` and ending with `}`.
All the parameters (except `id`) are written as `key: value` inside and separated by `,`.
The reference refers to them as `config.key`, i.e. for the following one as `config.control` but you use them like below.
## Control
`control` determines which parameter will be modulated. This allows you to place your `lfo` at different places,
not necessarily immediately after the controlled parameter.
Here, we place `lfo` after the `s`, but it modulates the low pass filter.
<MiniRepl
client:idle
tune={`
s("saw")
.lfo({control:'lpf'})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
You can even influence parameters which are always present, even if you haven't explicitly written them, like `gain`.
<MiniRepl
client:idle
tune={`
s("saw")
.lfo({control:'gain'})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
`control` has an alias `c`.
## Rate
`rate` determines how often the `lfo` oscillates per second:
<MiniRepl
client:idle
tune={`
s("saw")
.lfo({c:'gain', rate:"<2 4>"})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
The alias of `rate` is `r`.{' '}
## Sync
Instead of controlling the frequency with `rate` by setting a frequency in Hz, you can use `sync`
to snychronize your lfo with your other patterns.
`sync` expresses this frequency as "times per cycle"
Try removing the `sync: "<2 4 8 0.5 >"` from the pattern and notice that there is something not in sync.
<MiniRepl
client:idle
tune={`
$: s("bd*4").bank("TR909").postgain(0.5)
$: s("saw")
.lpf(800)
.lfo({sync: "<2 4 8 0.5 >"})
._spectrum({height: 300, width: 800})`}
/>
You can put patterns into the parameters of `lfo` if you want them to change over time, as seen above.
## Relative Depth
`.depth` is relative depth, a value of 1 (the default) means that
the value goes above and below by half of the value which is being modulated
E.g. for depth `1`, it modulates the frequency to oscillate between 32 = (64/2) and 96 (= 64 + 64/2).
<MiniRepl
client:idle
tune={`
s("saw").freq(64)
.lfo({r: 2, depth: "<1 2 3>"})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
The `freq` is not needed here, as this is the default frequency, it's just for instructive purposes.
The aliases of `depth` are `dr` and `dep`.
## Absolute Depth
`depthabs` controls the absolute modulation depth. For example you can modulate
the low pass filter by exactly 250 Hz up and below:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, depthabs: "250"})`}
/>
The alias of `depthabs` is `da`.
## DC offset
If you don't want to go up or down by the same amount with your modulation, then you can shift the center of the modulation with `dcoffset`.
The default value is -0.5, which is the middle point between:
"dcoffset = 0: All modulations increase the control parameter (or keep it constant)"
"dcoffset = -1: All modulations decrease the control parameter (or keep it constant)"
Other values will work as well.
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(600)
.lfo({r: 2, da: "200", dcoffset: "<-0.5 -1 -0.5 0>"})`}
/>
The marvellous alias of `dcoffset` is `dc`.
## Shape
You can change the shape of the modulation with `shape`. The default is `triangle`, but other shapes are available too:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, shape: "<triangle sine ramp saw square>"}) `}
/>
You can add `._spectrum()` to see the shape of the modulations in the spectrum.
You can also get these shapes by using numbers:
| Shape | number |
| -------- | ------ |
| triangle | 0 |
| sine | 1 |
| ramp | 2 |
| saw | 3 |
| square | 4 |
this way, you can use a function like `irand` to generate numbers:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, shape: irand(4)}) `}
/>
The alias of `shape` is `sh`.
## Skewing some shapes
You can influence some of the shapes (`triangle` and `square`) in more detail.
The default skew is 0.5, it does different things for these two.
For `triangle`, it skews the top of the triangle to the left or right,
where 0 makes it look like `saw` and 1 makes it look like `ramp`.
For `square`, the skew influences the pulse width (see reference for `pulse` and `.pw`):
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, sh: "<triangle square>/5",
skew: "<0 0.25 0.5 0.75 1>"}) `}
/>
The alias of `skew` is `sk`.{' '}
## Curve
You can change the curves of your lfo and can make it more intense.
The default value is 1. Writing numbers greater than one can make it more intense,
numbers between 0 and 1 will make it less intense.
This will raise the lfo to the power of curve, so larger numbers (such as 10) can have very unexpected results.
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, sh: "<triangle>", curve: "<1.3 1 0.75 1>"}) `}
/>
## Referencing your lfos with id
All the lfos are numbered from the first starting with 0, the second having an `id` of 1 and so on.
You can refer to this in a later call if you want to modify a specific `lfo`.
The `id` is outside of the config json (which is different from the other parameters of `lfo()`
Try out how the sound changes when you replace the 0 with a 1.
<MiniRepl
client:idle
tune={`
s("saw").lfo().lpf(800)
.lfo({s: "<4 8 0.5>"})
.sometimes(x => x.lfo({dr: "4"},0))`}
/>
You can also name your lfos and refer to them by name, using the `id` parameter
<MiniRepl
client:idle
tune={`
s("saw").lfo({}, "lfo_freq_saw").lpf(800)
.lfo({s: "<4 8 0.5>"}, "lfo_lpf")
.sometimes(x => x.lfo({dr: "4"},"<lfo_lpf lfo_freq_saw>"))`}
/>
## FX index
If you are using `FX()` to reorder your effects, you dont need to write your lfos inside the `FX`,
but instead can refer to them by their FX index (starting with 0)
<MiniRepl
client:idle
tune={`
s("saw").FX(
distort(3),
gain(0.3), // this has the fx index 1
lpf(400)
).lfo({s: 16, dr:2, c:"gain", fxi: 1})`}
/>
## Modulating other LFOs with Sub-control
LFOs can modulate other lfos and will modulate their frequency (given by `r` or `s`):
<MiniRepl
client:idle
tune={`
s("saw").lpf(400).gain(0.8)
.lfo({s: 16, dr:2, c:"gain"})
.lfo({s: 0.3, dc:-1, dr: 0.8})`}
/>
To modulate other parameters of the first lfo (like `skew`, `depth` and so on), we can specify this with `subControl`or
its alias `sc`
<MiniRepl
client:idle
tune={`
s("saw").lpf(400).gain(0.8)
.lfo({s: 4, dr:2, c:"gain"})
.lfo({s: 0.3, sc: "skew"})`}
/>