mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-19 00:56:43 -04:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 01abc8ae79 | |||
| 84df683794 | |||
| ca1aca0c00 | |||
| e8668a1552 | |||
| 86d65c72f0 | |||
| 9f19e4616a | |||
| 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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build and Deploy to beta (warm.strudel.cc)
|
||||
name: Build and Deploy to warm (strudel.cc)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -37,4 +37,4 @@ jobs:
|
||||
apt update && apt install -y rsync
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build and Deploy to live (strudel.cc)
|
||||
name: Build and Deploy to cold (cold.strudel.cc)
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
@@ -34,4 +34,4 @@ jobs:
|
||||
apt update && apt install -y rsync
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/cold.strudel.cc
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
@@ -3324,6 +3359,11 @@ Pattern.prototype.shrinklist = function (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*
|
||||
*
|
||||
@@ -4092,3 +4132,59 @@ Pattern.prototype.worklet = function (src, ...inputs) {
|
||||
};
|
||||
|
||||
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 = [];
|
||||
/**
|
||||
* 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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+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/>.
|
||||
*/
|
||||
|
||||
import { Pattern, isPattern } from '@strudel/core';
|
||||
import { Pattern, isPattern, createParams } from '@strudel/core';
|
||||
import Paho from 'paho-mqtt';
|
||||
|
||||
const connections = {};
|
||||
@@ -61,7 +61,6 @@ Pattern.prototype.mqtt = function (
|
||||
const props = {
|
||||
onSuccess: onConnect,
|
||||
onFailure: onFailure,
|
||||
useSSL: true,
|
||||
};
|
||||
|
||||
if (username) {
|
||||
@@ -119,3 +118,12 @@ Pattern.prototype.mqtt = function (
|
||||
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)) {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`] = `
|
||||
[
|
||||
"[ 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`] = `
|
||||
[
|
||||
"[ 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 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 +12679,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/3 → 1/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/1 → 3/1 | s:gm_flute stretch:0 ]",
|
||||
"[ 3/1 → 4/1 | s:gm_flute stretch:-2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
|
||||
@@ -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');
|
||||
|
||||
`}
|
||||
/>
|
||||
@@ -44,6 +44,25 @@ export function LogoButton({ context, isEmbedded }) {
|
||||
|
||||
export function MainPanel({ context, isEmbedded = false, className }) {
|
||||
const { isZen, isButtonRowHidden, fontFamily } = useSettings();
|
||||
let loc = window.location;
|
||||
let ver = 'unofficial';
|
||||
let hot = false;
|
||||
let b = loc.hostname.match(/^(.+)\.(strudel)/);
|
||||
if (/(strudel.cc$)/.test(loc.hostname)) {
|
||||
// if there's no text before 'strudel', it's warm, otherwise use the text before strudel
|
||||
ver = b ? b[1] : 'warm';
|
||||
} else {
|
||||
// match both versions of localhost
|
||||
if (/(localhost)|(127.0.0.1)/.test(loc.hostname)) ver = 'dev';
|
||||
}
|
||||
let pr = ver.match(/pr-([0-9]+)/);
|
||||
if (pr) {
|
||||
pr = pr[1];
|
||||
ver = `hot: ${pr}`;
|
||||
hot = true;
|
||||
pr = `https://codeberg.org/uzu/strudel/pulls/${pr}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
id="header"
|
||||
@@ -72,6 +91,13 @@ export function MainPanel({ context, isEmbedded = false, className }) {
|
||||
<div className="space-x-2 flex items-baseline">
|
||||
<span className="hidden sm:block">strudel</span>
|
||||
<span className="text-sm font-medium hidden sm:block">REPL</span>
|
||||
{!hot ? (
|
||||
<span className="text-sm font-medium hidden sm:block">({ver})</span>
|
||||
) : (
|
||||
<a className="hover:opacity-50" href={pr} target="_blank">
|
||||
<span className="text-sm font-medium hidden sm:block">({ver})</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
Reference in New Issue
Block a user