mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-29 16:02:51 -04:00
Compare commits
69 Commits
move-domains
...
name
| Author | SHA1 | Date | |
|---|---|---|---|
| a866cb2189 | |||
| 890d69b23e | |||
| 8e187abf24 | |||
| 2d014b47c8 | |||
| daafee7527 | |||
| c150752372 | |||
| e248bf85f3 | |||
| 6c51c0261a | |||
| deefde7b50 | |||
| d4f63e8de3 | |||
| 19cb3dedc2 | |||
| a7c3407da7 | |||
| 6870b04fb2 | |||
| 2b2646a768 | |||
| 97ef5bc335 | |||
| 85e6d436ef | |||
| d3e2b7c7b4 | |||
| def1738259 | |||
| 0feeb1e701 | |||
| 813cc6c190 | |||
| 62d4f84e69 | |||
| 49a1e11cd8 | |||
| 941c97da0d | |||
| 95a9d301a1 | |||
| 475f17ddfd | |||
| bd68c6a0a7 | |||
| 2635716c87 | |||
| 58f956b57e | |||
| 0ae2c120b3 | |||
| 05a43ef687 | |||
| f21aeb55bd | |||
| d83139980b | |||
| 0c61cd7670 | |||
| 13b11efa15 | |||
| f73b395648 | |||
| 9c47efd3cf | |||
| f4066089f4 | |||
| 0e81987c4e | |||
| 23dbe53013 | |||
| 0e4d0484a3 | |||
| 37bf2e4dc7 | |||
| e69761397e | |||
| 10c33f24f5 | |||
| ea0ec7edb9 | |||
| f900642359 | |||
| 4cc63b8b01 | |||
| 26832c0fe7 | |||
| ae32743f12 | |||
| 7294cd5443 | |||
| cdae661b24 | |||
| 63f42faca8 | |||
| 2624b97500 | |||
| 633b7287c0 | |||
| 3805ba8f76 | |||
| 82e88d4a28 | |||
| 5cd45d5ea1 | |||
| d930057000 | |||
| daf27ed971 | |||
| 8520044f3e | |||
| 7c22b29493 | |||
| c77675c492 | |||
| 5197af1aa4 | |||
| 85f6472894 | |||
| 54fd877be1 | |||
| 7d948fd1a7 | |||
| 3373fa39a6 | |||
| 1de8123437 | |||
| 781c477078 | |||
| e2cdce5ac9 |
@@ -0,0 +1,11 @@
|
|||||||
|
**/node_modules
|
||||||
|
**/.angular
|
||||||
|
**/dist
|
||||||
|
.astro/
|
||||||
|
.forgejo/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
test/
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
+11
-1
@@ -52,7 +52,17 @@ Strudel is a project handmade by humans, with thought and nuance.
|
|||||||
|
|
||||||
If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself.
|
If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself.
|
||||||
|
|
||||||
There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels.
|
There are #llm-chat and #llm-share channels on [our discord](https://discord.com/invite/HGEdXmRkzT). Please do not discuss or share LLM-related things outside of those channels.
|
||||||
|
|
||||||
|
## Creating and sharing a new project using strudel
|
||||||
|
|
||||||
|
Strudel is free/open source software, and we are also happy to see people making use of it within the following terms.
|
||||||
|
|
||||||
|
Please don't use 'strudel' in the name of your project, so people don't assume it's official strudel project. (If you'd like it to be an official strudel project, please check in with the community, e.g. on [the discord](https://discord.com/invite/HGEdXmRkzT).)
|
||||||
|
|
||||||
|
Please respect our AGPL license, which e.g. requires you to share/link to the source code of strudel, any modifications you've made to it, and the source code for the rest of your project if it integrates with strudel. You are also required to maintain Strudel's copyright notices in the source code, and include Strudel's copyright notice in your user interface. This is an ad-hoc summary - please [refer to the license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) for full details.
|
||||||
|
|
||||||
|
You are also encouraged to connect with the community and understand our aims and values.
|
||||||
|
|
||||||
## Report a Bug
|
## Report a Bug
|
||||||
|
|
||||||
|
|||||||
@@ -2582,13 +2582,17 @@ export const { compressorRelease } = registerControl('compressorRelease');
|
|||||||
export const { speed } = registerControl('speed');
|
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
|
* @name stretch
|
||||||
* @tags pitch, samples
|
* @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
|
* @example
|
||||||
* s("gm_flute").stretch("1 2 .5")
|
* s("gm_flute").stretch("<2 1 0 -2>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { stretch } = registerControl('stretch');
|
export const { stretch } = registerControl('stretch');
|
||||||
|
|||||||
@@ -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()) {
|
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)) {
|
if (Array.isArray(name)) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name_item of name) {
|
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);
|
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.
|
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
|
||||||
* @tags temporal, superdough
|
* @tags temporal, superdough
|
||||||
@@ -2560,6 +2580,21 @@ export const jux = register('jux', function (func, pat) {
|
|||||||
return pat._juxBy(1, 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.
|
* Superimpose and offset multiple times, applying the given function each time.
|
||||||
* @tags temporal, functional
|
* @tags temporal, functional
|
||||||
@@ -3324,6 +3359,11 @@ Pattern.prototype.shrinklist = function (amount) {
|
|||||||
|
|
||||||
export const shrinklist = (amount, pat) => pat.shrinklist(amount);
|
export const shrinklist = (amount, pat) => pat.shrinklist(amount);
|
||||||
|
|
||||||
|
Pattern.prototype.growlist = function (amount) {
|
||||||
|
return this.shrinklist(amount).reverse();
|
||||||
|
};
|
||||||
|
export const growlist = (amount, pat) => pat.growlist(amount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
@@ -4092,3 +4132,59 @@ Pattern.prototype.worklet = function (src, ...inputs) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const worklet = (...args) => pure({}).worklet(...args);
|
export const worklet = (...args) => pure({}).worklet(...args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a pattern of numbers in base b from a number or pattern of numbers
|
||||||
|
* limited to d digits long from the right
|
||||||
|
*
|
||||||
|
* @name base
|
||||||
|
* @tags generators
|
||||||
|
* @param {number} n - number to convert (can be a pattern or array)
|
||||||
|
* @param {number} b - base to convert to (defaults to 10) (can be a pattern)
|
||||||
|
* @param {number} d - max number of digits to produce for each n (defaults to 0 for all) (can be a pattern)
|
||||||
|
* @example
|
||||||
|
* $: note(base("7175 543", 10, 3)).scale("c:major").s("saw")
|
||||||
|
* // $: note("1 7 5 5 4 3").scale("c:major").s("saw")
|
||||||
|
*/
|
||||||
|
export const base = (n, b = 10, d = 0) => {
|
||||||
|
if (Array.isArray(n)) {
|
||||||
|
n = sequence(n);
|
||||||
|
}
|
||||||
|
n = reify(n);
|
||||||
|
b = reify(b);
|
||||||
|
d = reify(d);
|
||||||
|
|
||||||
|
return d
|
||||||
|
.withValue((e) => {
|
||||||
|
return b
|
||||||
|
.withValue((c) => {
|
||||||
|
return n
|
||||||
|
.withValue((v) => {
|
||||||
|
let digits = [];
|
||||||
|
let value = v;
|
||||||
|
while (value > 0) {
|
||||||
|
digits.unshift(value % c);
|
||||||
|
value = Math.floor(value / c);
|
||||||
|
}
|
||||||
|
if (e) {
|
||||||
|
const l = digits.length;
|
||||||
|
if (l > e) {
|
||||||
|
digits = digits.slice(-1 * e);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if (l < e){
|
||||||
|
for (let i = l; i < e; i++) {
|
||||||
|
digits.unshift("~");//0); //Would like to be padding this but ~- doesn't work
|
||||||
|
}
|
||||||
|
console.log("digits", digits);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
return sequence(digits);
|
||||||
|
})
|
||||||
|
.squeezeJoin();
|
||||||
|
})
|
||||||
|
.squeezeJoin();
|
||||||
|
})
|
||||||
|
.squeezeJoin();
|
||||||
|
};
|
||||||
|
|||||||
@@ -307,8 +307,12 @@ export function repl({
|
|||||||
|
|
||||||
let allTransforms = [];
|
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.
|
* 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("bd - cp sd")
|
||||||
* $: sound("hh*8")
|
* $: 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.
|
/** 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("bd - cp sd")
|
||||||
* $: sound("hh*8")
|
* $: sound("hh*8")
|
||||||
|
|||||||
@@ -537,6 +537,9 @@ export async function midin(input) {
|
|||||||
* The note length is fixed as Superdough is not currently set up for undetermined
|
* The note length is fixed as Superdough is not currently set up for undetermined
|
||||||
* note durations
|
* 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
|
* @name midikeys
|
||||||
* @tags external_io, midi
|
* @tags external_io, midi
|
||||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||||
@@ -552,6 +555,10 @@ export async function midin(input) {
|
|||||||
* .s("saw")
|
* .s("saw")
|
||||||
* .add(note(rand.mul(0.3)))
|
* .add(note(rand.mul(0.3)))
|
||||||
* .lpf(1000).lpe(2).room(0.5)
|
* .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 kHaps = {};
|
||||||
const kListeners = {};
|
const kListeners = {};
|
||||||
@@ -633,7 +640,7 @@ export async function midikeys(input) {
|
|||||||
*/
|
*/
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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, {}));
|
kHaps[input].push(new Hap(span, span, value, {}));
|
||||||
if (!noteoff && triggerAvailable) {
|
if (!noteoff && triggerAvailable) {
|
||||||
|
|||||||
+10
-2
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, isPattern } from '@strudel/core';
|
import { Pattern, isPattern, createParams } from '@strudel/core';
|
||||||
import Paho from 'paho-mqtt';
|
import Paho from 'paho-mqtt';
|
||||||
|
|
||||||
const connections = {};
|
const connections = {};
|
||||||
@@ -61,7 +61,6 @@ Pattern.prototype.mqtt = function (
|
|||||||
const props = {
|
const props = {
|
||||||
onSuccess: onConnect,
|
onSuccess: onConnect,
|
||||||
onFailure: onFailure,
|
onFailure: onFailure,
|
||||||
useSSL: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
@@ -119,3 +118,12 @@ Pattern.prototype.mqtt = function (
|
|||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This adds the 'move' and 'motor' commands to strudel
|
||||||
|
export const { move, motor } = createParams('move', 'motor');
|
||||||
|
window.move = move;
|
||||||
|
window.motor = motor;
|
||||||
|
// This adds the 'robot' command
|
||||||
|
Pattern.prototype.robot = function (robot_id, address = 'ws://192.168.8.248:9001/mqtt') {
|
||||||
|
return this.mqtt(undefined, undefined, '/move/' + robot_id, address);
|
||||||
|
};
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
if (isNote(controls.note)) {
|
if (isNote(controls.note)) {
|
||||||
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
||||||
} else {
|
} else {
|
||||||
controls.note = parseNumeral(controls.note);
|
controls.midinote = parseNumeral(controls.note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controls.bank && (controls.s = controls.bank + controls.s);
|
controls.bank && (controls.s = controls.bank + controls.s);
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export const withBase = register('withBase', (b, pat) => {
|
|||||||
let hVal = hap.value;
|
let hVal = hap.value;
|
||||||
const isObject = typeof hVal === 'object';
|
const isObject = typeof hVal === 'object';
|
||||||
let freq = isObject ? hVal.freq : hVal;
|
let freq = isObject ? hVal.freq : hVal;
|
||||||
|
if (!freq) return hap;
|
||||||
freq = (freq * base) / originalBase;
|
freq = (freq * base) / originalBase;
|
||||||
hap.value = isObject ? { ...hap.value, freq } : { freq };
|
hap.value = isObject ? { ...hap.value, freq } : { freq };
|
||||||
return hap;
|
return hap;
|
||||||
|
|||||||
@@ -1242,6 +1242,35 @@ exports[`runs examples > example "bank" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "base" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/6 | note:D3 s:saw ]",
|
||||||
|
"[ 1/6 → 1/3 | note:C4 s:saw ]",
|
||||||
|
"[ 1/3 → 1/2 | note:A3 s:saw ]",
|
||||||
|
"[ 1/2 → 2/3 | note:A3 s:saw ]",
|
||||||
|
"[ 2/3 → 5/6 | note:G3 s:saw ]",
|
||||||
|
"[ 5/6 → 1/1 | note:F3 s:saw ]",
|
||||||
|
"[ 1/1 → 7/6 | note:D3 s:saw ]",
|
||||||
|
"[ 7/6 → 4/3 | note:C4 s:saw ]",
|
||||||
|
"[ 4/3 → 3/2 | note:A3 s:saw ]",
|
||||||
|
"[ 3/2 → 5/3 | note:A3 s:saw ]",
|
||||||
|
"[ 5/3 → 11/6 | note:G3 s:saw ]",
|
||||||
|
"[ 11/6 → 2/1 | note:F3 s:saw ]",
|
||||||
|
"[ 2/1 → 13/6 | note:D3 s:saw ]",
|
||||||
|
"[ 13/6 → 7/3 | note:C4 s:saw ]",
|
||||||
|
"[ 7/3 → 5/2 | note:A3 s:saw ]",
|
||||||
|
"[ 5/2 → 8/3 | note:A3 s:saw ]",
|
||||||
|
"[ 8/3 → 17/6 | note:G3 s:saw ]",
|
||||||
|
"[ 17/6 → 3/1 | note:F3 s:saw ]",
|
||||||
|
"[ 3/1 → 19/6 | note:D3 s:saw ]",
|
||||||
|
"[ 19/6 → 10/3 | note:C4 s:saw ]",
|
||||||
|
"[ 10/3 → 7/2 | note:A3 s:saw ]",
|
||||||
|
"[ 7/2 → 11/3 | note:A3 s:saw ]",
|
||||||
|
"[ 11/3 → 23/6 | note:G3 s:saw ]",
|
||||||
|
"[ 23/6 → 4/1 | note:F3 s:saw ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "beat" example index 0 1`] = `
|
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd ]",
|
"[ 0/1 → 1/16 | s:bd ]",
|
||||||
@@ -6527,6 +6556,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`] = `
|
exports[`runs examples > example "keep" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c s:piano ]",
|
"[ 0/1 → 1/4 | note:c s:piano ]",
|
||||||
@@ -7683,6 +7956,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 1 1`] = `[]`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midikeys" example index 2 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
@@ -12404,18 +12679,10 @@ exports[`runs examples > example "stepcat" example index 1 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "stretch" example index 0 1`] = `
|
exports[`runs examples > example "stretch" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ (0/1 → 1/3) ⇝ 1/1 | s:gm_flute stretch:1 ]",
|
"[ 0/1 → 1/1 | s:gm_flute stretch:2 ]",
|
||||||
"[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | s:gm_flute stretch:2 ]",
|
"[ 1/1 → 2/1 | s:gm_flute stretch:1 ]",
|
||||||
"[ 0/1 ⇜ (2/3 → 1/1) | s:gm_flute stretch:0.5 ]",
|
"[ 2/1 → 3/1 | s:gm_flute stretch:0 ]",
|
||||||
"[ (1/1 → 4/3) ⇝ 2/1 | s:gm_flute stretch:1 ]",
|
"[ 3/1 → 4/1 | s:gm_flute stretch:-2 ]",
|
||||||
"[ 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 ]",
|
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export default defineConfig({
|
|||||||
urlPattern: ({ url }) =>
|
urlPattern: ({ url }) =>
|
||||||
[
|
[
|
||||||
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
||||||
|
/^https:\/\/strudel\.b-cdn\.net\/.*/i,
|
||||||
/^https:\/\/freesound\.org\/.*/i,
|
/^https:\/\/freesound\.org\/.*/i,
|
||||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const SIDEBAR: Sidebar = {
|
|||||||
{ text: 'Random Modifiers', link: 'learn/random-modifiers' },
|
{ text: 'Random Modifiers', link: 'learn/random-modifiers' },
|
||||||
{ text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' },
|
{ text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' },
|
||||||
{ text: 'Accumulation', link: 'learn/accumulation' },
|
{ text: 'Accumulation', link: 'learn/accumulation' },
|
||||||
|
{ text: 'LFOs', link: 'learn/lfo' },
|
||||||
{ text: 'Tonal Functions', link: 'learn/tonal' },
|
{ text: 'Tonal Functions', link: 'learn/tonal' },
|
||||||
{ text: 'Stepwise Functions', link: 'learn/stepwise' },
|
{ text: 'Stepwise Functions', link: 'learn/stepwise' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ There are multiple ways to load your sample collection. Some methods are good fo
|
|||||||
- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed.
|
- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed.
|
||||||
- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples)
|
- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples)
|
||||||
|
|
||||||
|
## Can I create a new project based on Strudel?
|
||||||
|
|
||||||
|
Strudel is free/open source software, and we are always happy to see people making use of it within the following terms:
|
||||||
|
|
||||||
|
- Please don't use 'strudel' in the name of your project (e.g. strudel2000, foo-strudel), so people don't assume it's official strudel project. (If you'd like it to be an official strudel project, please check in with the community, e.g. on the [discord chat](https://discord.com/invite/HGEdXmRkzT).)
|
||||||
|
- Please respect our AGPL license, which e.g. requires you to share/link to the source code of strudel, any modifications you've made to it, and the source code for the rest of your project if it integrates with strudel. You are also required to maintain Strudel's copyright notices in the source code, and include Strudel's copyright notice in your user interface. This is an ad-hoc summary - please [refer to the license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) for full details.
|
||||||
|
|
||||||
|
You are also encouraged to connect with [the community](https://discord.com/invite/HGEdXmRkzT) to understand our aims and values.
|
||||||
|
|
||||||
## Can I use Strudel with AI/LLM tools?
|
## Can I use Strudel with AI/LLM tools?
|
||||||
|
|
||||||
You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license.
|
You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license.
|
||||||
|
|||||||
@@ -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"})`}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
title: Movement with Strudel
|
||||||
|
layout: ../../layouts/MainLayout.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
import { MiniRepl } from '@src/docs/MiniRepl';
|
||||||
|
import Box from '@components/Box.astro';
|
||||||
|
import QA from '@components/QA';
|
||||||
|
|
||||||
|
# Controlling motors with Strudel
|
||||||
|
|
||||||
|
Strudel is mainly made for making music, but it's possible to pattern other things with it, including motors.
|
||||||
|
|
||||||
|
We're going to use a microcontroller for this, called an "[Inventor 2040W](https://shop.pimoroni.com/products/inventor-2040-w)", which is
|
||||||
|
a [Pico W](https://shop.pimoroni.com/products/inventor-2040-w?variant=40053063155795) with extra ports added including some for controlling motors.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Technical details
|
||||||
|
|
||||||
|
Feel free to gloss over these!
|
||||||
|
|
||||||
|
- The Inventor 2040W connects to the internet wirelessly, and it can power from a battery or USB. Hopefully the batteries last!
|
||||||
|
- It's running [some code](https://github.com/patternclub/alpacalab/blob/main/course/main.py) that listens for messages using an "Internet of Things" network protocol (called MQTT). When it receives a message, it moves a motor.
|
||||||
|
- It connects to a small server (running software called 'mosquitto') on Alex's laptop.
|
||||||
|
- Strudel can send these messages instead of triggering sounds - that's how we use it to pattern movement.
|
||||||
|
|
||||||
|
## First movement
|
||||||
|
|
||||||
|
Let's get a motor running!
|
||||||
|
|
||||||
|
1. Note the letter drawn on a label on the back of the microcontroller.
|
||||||
|
|
||||||
|
2. Plug a battery into your microcontroller.
|
||||||
|
|
||||||
|
3. Plug a motor into 'servo' (not motor) plug numbered 1, with the yellow (lightest) cable closest to the '1', and the brown (darkest) cable outward
|
||||||
|
|
||||||
|
4. Run the below to set up some values, changing the `x` in 'robot('x')` to the letter on your microcontroller.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
$: move("-60 80").motor("0").robot('x');
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
|
||||||
|
If you refresh the page, you'll need to change the letter to match your robot again.
|
||||||
|
|
||||||
|
If your motor starts moving unexpectedly, someone else might have put your letter in by mistake!
|
||||||
|
|
||||||
|
Note that in the above, we start counting motors from '0', so motor 1 on the board is motor 0 in the code.
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
## Patterning movement
|
||||||
|
|
||||||
|
Many strudel features for playing with sound patterns will work when
|
||||||
|
playing with motor patterns. Try playing with the mininotation in the
|
||||||
|
`move` command:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
$: move("-10 0 10 [20 30]*2").motor("0").slow(2).robot('x')
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
The move instructions are in the range from -90 to 90.
|
||||||
|
|
||||||
|
<box>
|
||||||
|
If your motors stop working at some point, and your code looks right, try pressing the 'reset' button on the
|
||||||
|
microcontroller.
|
||||||
|
</box>
|
||||||
|
|
||||||
|
It's possible to make smooth movements based on different 'waveforms', for example a smooth sinewave:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
$: move(sine.range(-30, 30).segment(16)).motor("0").slow(2).robot('x');
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
The movement is still quite jerky, because the 'segment' command is
|
||||||
|
only taking 16 positions from the sinewave. Try increasing it to 32 or 64. It's best not too much higher than that, as the microcontroller
|
||||||
|
might get overwhelmed with a backlog of instructions!
|
||||||
|
|
||||||
|
<box>
|
||||||
|
Try replacing `sine` with other waveforms: `saw` (sawtooth wave), `tri` (triangular wave) are good, and there is also
|
||||||
|
`rand` (random wave) and `perlin` (a kind of smoothed-out randomness).
|
||||||
|
</box>
|
||||||
|
|
||||||
|
## Patterning more than one motor
|
||||||
|
|
||||||
|
You can pattern the `motor` command separately from the `move` one:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
$: move("-10 0 10 [20 30]*2").motor("0 1").slow(2).robot('x');
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Alternatively, you can pattern two motors in separate patterns. The below sends the same pattern for the first two motors, but with the second one running slower:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
|
||||||
|
$: move("-10 0 10 [20 30]\*2").motor("0").slow(2).robot('x');
|
||||||
|
|
||||||
|
$: move("-10 0 10 [20 30]\*2").motor("1").slow(3).robot('x');
|
||||||
|
|
||||||
|
`}
|
||||||
|
/>
|
||||||
@@ -36,8 +36,9 @@ export function WelcomeTab({ context }) {
|
|||||||
<a href="https://tidalcycles.org/" target="_blank">
|
<a href="https://tidalcycles.org/" target="_blank">
|
||||||
tidalcycles
|
tidalcycles
|
||||||
</a>
|
</a>
|
||||||
, which is a popular live coding language for music, written in Haskell. Strudel is free/open source software:
|
, which is a popular live coding language for music, written in Haskell. Strudel is free/open source software,
|
||||||
you can redistribute and/or modify it under the terms of the{' '}
|
with copyright owned by its [contributors](https://codeberg.org/uzu/strudel/activity/contributors). You can
|
||||||
|
redistribute and/or modify it under the terms of the{' '}
|
||||||
<a href="https://codeberg.org/uzu/strudel/src/branch/main/LICENSE" target="_blank">
|
<a href="https://codeberg.org/uzu/strudel/src/branch/main/LICENSE" target="_blank">
|
||||||
GNU Affero General Public License
|
GNU Affero General Public License
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user