merge main

This commit is contained in:
Jade (Rose) Rowland 2024-12-30 21:59:40 -05:00
commit 1da80db048
30 changed files with 766 additions and 319 deletions

View file

@ -14,10 +14,16 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally: After cloning the project, you can run the REPL locally:
```bash 1. Install [Node.js](https://nodejs.org/)
pnpm i 2. Install [pnpm](https://pnpm.io/installation)
pnpm dev 3. Install dependencies by running the following command:
``` ```bash
pnpm i
```
4. Run the development server:
```bash
pnpm dev
```
## Using Strudel In Your Project ## Using Strudel In Your Project

View file

@ -133,6 +133,7 @@ export class StrudelMirror {
autodraw, autodraw,
prebake, prebake,
bgFill = true, bgFill = true,
solo = true,
...replOptions ...replOptions
} = options; } = options;
this.code = initialCode; this.code = initialCode;
@ -143,6 +144,7 @@ export class StrudelMirror {
this.drawContext = drawContext; this.drawContext = drawContext;
this.onDraw = onDraw || this.draw; this.onDraw = onDraw || this.draw;
this.id = id || s4(); this.id = id || s4();
this.solo = solo;
this.drawer = new Drawer((haps, time, _, painters) => { this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time)); const currentFrame = haps.filter((hap) => hap.isActive(time));
@ -159,12 +161,14 @@ export class StrudelMirror {
replOptions?.onToggle?.(started); replOptions?.onToggle?.(started);
if (started) { if (started) {
this.drawer.start(this.repl.scheduler); this.drawer.start(this.repl.scheduler);
// stop other repls when this one is started if (this.solo) {
document.dispatchEvent( // stop other repls when this one is started
new CustomEvent('start-repl', { document.dispatchEvent(
detail: this.id, new CustomEvent('start-repl', {
}), detail: this.id,
); }),
);
}
} else { } else {
this.drawer.stop(); this.drawer.stop();
updateMiniLocations(this.editor, []); updateMiniLocations(this.editor, []);
@ -219,7 +223,7 @@ export class StrudelMirror {
// stop this repl when another repl is started // stop this repl when another repl is started
this.onStartRepl = (e) => { this.onStartRepl = (e) => {
if (e.detail !== this.id) { if (this.solo && e.detail !== this.id) {
this.stop(); this.stop();
} }
}; };

View file

@ -133,3 +133,10 @@ registerWidget('_pitchwheel', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d'); const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pitchwheel({ ...options, ctx, id }); return pat.pitchwheel({ ...options, ctx, id });
}); });
registerWidget('_spectrum', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.spectrum({ ...options, ctx, id });
});

View file

@ -68,6 +68,10 @@ export class Cyclist {
// see https://github.com/tidalcycles/strudel/pull/1004 // see https://github.com/tidalcycles/strudel/pull/1004
const deadline = targetTime - phase; const deadline = targetTime - phase;
onTrigger?.(hap, deadline, duration, this.cps, targetTime); onTrigger?.(hap, deadline, duration, this.cps, targetTime);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
this.cps = hap.value.cps;
this.num_ticks_since_cps_change = 0;
}
} }
}); });
} catch (e) { } catch (e) {

View file

@ -14,6 +14,7 @@ export * from './controls.mjs';
export * from './hap.mjs'; export * from './hap.mjs';
export * from './pattern.mjs'; export * from './pattern.mjs';
export * from './signal.mjs'; export * from './signal.mjs';
export * from './pick.mjs';
export * from './state.mjs'; export * from './state.mjs';
export * from './timespan.mjs'; export * from './timespan.mjs';
export * from './util.mjs'; export * from './util.mjs';

View file

@ -2593,7 +2593,7 @@ export const steps = register('steps', function (targetTactus, pat) {
// avoid divide by zero.. // avoid divide by zero..
return nothing; return nothing;
} }
return pat.fast(Fraction(targetTactus).div(pat.tactus)); return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus);
}); });
export function _polymeterListSteps(steps, ...args) { export function _polymeterListSteps(steps, ...args) {
@ -2801,7 +2801,7 @@ export const s_sub = stepRegister('s_sub', function (i, pat) {
return pat.s_add(pat.tactus.sub(i)); return pat.s_add(pat.tactus.sub(i));
}); });
export const s_cycles = stepRegister('s_extend', function (factor, pat) { export const s_extend = stepRegister('s_extend', function (factor, pat) {
return pat.fast(factor).s_expand(factor); return pat.fast(factor).s_expand(factor);
}); });
@ -2882,6 +2882,13 @@ export const s_tour = function (pat, ...many) {
return pat.s_tour(...many); return pat.s_tour(...many);
}; };
const s_zip = function (...pats) {
pats = pats.filter((pat) => pat.hasTactus);
const zipped = slowcat(...pats.map((pat) => pat._slow(pat.tactus)));
// Should maybe use lcm or gcd for tactus?
return zipped._fast(pats[0].tactus).setTactus(pats[0].tactus);
};
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of // Control-related functions, i.e. ones that manipulate patterns of
// objects // objects

214
packages/core/pick.mjs Normal file
View file

@ -0,0 +1,214 @@
/*
pick.mjs - methods that use one pattern to pick events from other patterns.
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
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, reify, silence, register } from './pattern.mjs';
import { _mod, clamp, objectMap } from './util.mjs';
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a@2 b@2 c@2 d@2>".pickRestart({
a: n("0 1 2 0"),
b: n("2 3 4 ~"),
c: n("[4 5] [4 3] 2 0"),
d: n("0 -3 0 ~")
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};

View file

@ -63,11 +63,13 @@ export function repl({
let pPatterns = {}; let pPatterns = {};
let anonymousIndex = 0; let anonymousIndex = 0;
let allTransform; let allTransform;
let eachTransform;
const hush = function () { const hush = function () {
pPatterns = {}; pPatterns = {};
anonymousIndex = 0; anonymousIndex = 0;
allTransform = undefined; allTransform = undefined;
eachTransform = undefined;
return silence; return silence;
}; };
@ -84,15 +86,42 @@ export function repl({
const toggle = () => scheduler.toggle(); const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps); const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60); const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
/** 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
* a version that applies the function to each pattern separately.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* all(fast("<2 3>"))
* ```
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* all(x => x.pianoroll())
* ```
*/
const all = function (transform) { const all = function (transform) {
allTransform = transform; allTransform = transform;
return silence; return silence;
}; };
/** 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.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* each(fast("<2 3>"))
* ```
*/
const each = function (transform) {
eachTransform = transform;
return silence;
};
// set pattern methods that use this repl via closure // set pattern methods that use this repl via closure
const injectPatternMethods = () => { const injectPatternMethods = () => {
Pattern.prototype.p = function (id) { Pattern.prototype.p = function (id) {
if (id.startsWith('_') || id.endsWith('_')) { if (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) {
// allows muting a pattern x with x_ or _x // allows muting a pattern x with x_ or _x
return silence; return silence;
} }
@ -131,6 +160,7 @@ export function repl({
}); });
return evalScope({ return evalScope({
all, all,
each,
hush, hush,
cpm, cpm,
setCps, setCps,
@ -152,7 +182,14 @@ export function repl({
shouldHush && hush(); shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) { if (Object.keys(pPatterns).length) {
pattern = stack(...Object.values(pPatterns)); let patterns = Object.values(pPatterns);
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
}
pattern = stack(...patterns);
} else if (eachTransform) {
pattern = eachTransform(pattern);
} }
if (allTransform) { if (allTransform) {
pattern = allTransform(pattern); pattern = allTransform(pattern);

View file

@ -1,6 +1,6 @@
/* /*
signal.mjs - <short description TODO> signal.mjs - continuous patterns
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs> Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
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/>.
*/ */
@ -8,7 +8,7 @@ import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs'; import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import { id, _mod, clamp, objectMap, getCurrentKeyboardState } from './util.mjs'; import { id, getCurrentKeyboardState } from './util.mjs';
export function steady(value) { export function steady(value) {
// A continuous value // A continuous value
@ -254,211 +254,6 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
*/ */
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin(); export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a@2 b@2 c@2 d@2>".pickRestart({
a: n("0 1 2 0"),
b: n("2 3 4 ~"),
c: n("[4 5] [4 3] 2 0"),
d: n("0 -3 0 ~")
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};
export const __chooseWith = (pat, xs) => { export const __chooseWith = (pat, xs) => {
xs = xs.map(reify); xs = xs.map(reify);
if (xs.length == 0) { if (xs.length == 0) {
@ -597,8 +392,11 @@ export const perlinWith = (pat) => {
*/ */
export const perlin = perlinWith(time.fmap((v) => Number(v))); export const perlin = perlinWith(time.fmap((v) => Number(v)));
export const degradeByWith = register('degradeByWith', (withPat, x, pat) => export const degradeByWith = register(
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)), 'degradeByWith',
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
true,
true,
); );
/** /**
@ -615,9 +413,14 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
* @example * @example
* s("[hh?0.2]*8") * s("[hh?0.2]*8")
*/ */
export const degradeBy = register('degradeBy', function (x, pat) { export const degradeBy = register(
return pat._degradeByWith(rand, x); 'degradeBy',
}); function (x, pat) {
return pat._degradeByWith(rand, x);
},
true,
true,
);
/** /**
* *
@ -631,7 +434,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
* @example * @example
* s("[hh?]*8") * s("[hh?]*8")
*/ */
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5)); export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, true);
/** /**
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount. * Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
@ -651,12 +454,17 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
* x => x.undegradeBy(0.8).pan(1) * x => x.undegradeBy(0.8).pan(1)
* ) * )
*/ */
export const undegradeBy = register('undegradeBy', function (x, pat) { export const undegradeBy = register(
return pat._degradeByWith( 'undegradeBy',
rand.fmap((r) => 1 - r), function (x, pat) {
x, return pat._degradeByWith(
); rand.fmap((r) => 1 - r),
}); x,
);
},
true,
true,
);
/** /**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)` * Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
@ -673,7 +481,7 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
* x => x.undegrade().pan(1) * x => x.undegrade().pan(1)
* ) * )
*/ */
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5)); export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), true, true);
/** /**
* *

View file

@ -26,7 +26,7 @@ export const getDrawContext = (id = 'test-canvas', options) => {
}, 200); }, 200);
}); });
} }
return canvas.getContext(contextType); return canvas.getContext(contextType, { willReadFrequently: true });
}; };
let animationFrames = {}; let animationFrames = {};

3
packages/mqtt/README.md Normal file
View file

@ -0,0 +1,3 @@
# @strudel/serial
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.

87
packages/mqtt/mqtt.mjs Normal file
View file

@ -0,0 +1,87 @@
/*
mqtt.mjs - for patterning the internet of things from strudel
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/serial/serial.mjs>
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 Paho from 'paho-mqtt';
const connections = {};
// Handle connection loss
function onConnectionLost(responseObject) {
if (responseObject.errorCode !== 0) {
console.error(' mqtt connection lost: ', responseObject.errorMessage);
}
}
// Handle received messages
function onMessageArrived(message) {
console.log('incoming mqtt message: ', message.payloadString); // prettier-ignore
}
function onFailure(err) {
console.error('Connection failed: ', err);
}
Pattern.prototype.mqtt = function (
username = undefined,
password = undefined,
topic = undefined,
host = 'wss://localhost:8883/',
client = undefined,
latency = 0,
) {
const key = host + '-' + client;
let connected = false;
if (!client) {
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
}
function onConnect() {
console.log('Connected to mqtt broker');
connected = true;
}
let cx;
if (connections[key]) {
cx = connections[key];
} else {
cx = new Paho.Client(host, client);
cx.onConnectionLost = onConnectionLost;
cx.onMessageArrived = onMessageArrived;
const props = {
onSuccess: onConnect,
onFailure: onFailure,
useSSL: true,
};
if (username) {
props.userName = username;
props.password = password;
}
cx.connect(props);
}
return this.withHap((hap) => {
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
if (!connected) {
return;
}
let message = '';
if (typeof hap.value === 'object') {
message = JSON.stringify(hap.value);
} else {
message = hap.value;
}
message = new Paho.Message(message);
message.destinationName = topic;
const offset = (targetTime - currentTime + latency) * 1000;
window.setTimeout(function () {
cx.send(message);
}, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
});
};

View file

@ -0,0 +1,38 @@
{
"name": "@strudel/mqtt",
"version": "1.1.0",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Alex McLean <alex@slab.org>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"paho-mqtt": "^1.1.0"
},
"devDependencies": {
"vite": "^5.0.10"
}
}

View file

@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'mqtt.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});

View file

@ -10,6 +10,8 @@ if (typeof HTMLElement !== 'undefined') {
static observedAttributes = ['code']; static observedAttributes = ['code'];
settings = codemirrorSettings.get(); settings = codemirrorSettings.get();
editor = null; editor = null;
sync = false;
solo = true;
constructor() { constructor() {
super(); super();
} }
@ -49,6 +51,8 @@ if (typeof HTMLElement !== 'undefined') {
}); });
this.dispatchEvent(event); this.dispatchEvent(event);
}, },
solo: this.solo,
sync: this.sync,
}); });
// init settings // init settings
this.editor.updateSettings(this.settings); this.editor.updateSettings(this.settings);

View file

@ -270,11 +270,12 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
export let analysers = {}, export let analysers = {},
analysersData = {}; analysersData = {};
export function getAnalyserById(id, fftSize = 1024) { export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
if (!analysers[id]) { if (!analysers[id]) {
// make sure this doesn't happen too often as it piles up garbage // make sure this doesn't happen too often as it piles up garbage
const analyserNode = getAudioContext().createAnalyser(); const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize; analyserNode.fftSize = fftSize;
analyserNode.smoothingTimeConstant = smoothingTimeConstant;
// getDestination().connect(analyserNode); // getDestination().connect(analyserNode);
analysers[id] = analyserNode; analysers[id] = analyserNode;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);

View file

@ -6,4 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs'; export * from './webaudio.mjs';
export * from './scope.mjs'; export * from './scope.mjs';
export * from './spectrum.mjs';
export * from 'superdough'; export * from 'superdough';

View file

@ -0,0 +1,69 @@
import { Pattern, clamp } from '@strudel/core';
import { getDrawContext, getTheme } from '@strudel/draw';
import { analysers, getAnalyzerData } from 'superdough';
/**
* Renders a spectrum analyzer for the incoming audio signal.
* @name spectrum
* @param {object} config optional config with options:
* @param {integer} thickness line thickness in px (default 3)
* @param {integer} speed scroll speed (default 1)
* @param {integer} min min db (default -80)
* @param {integer} max max db (default 0)
* @example
* n("<0 4 <2 3> 1>*3")
* .off(1/8, add(n(5)))
* .off(1/5, add(n(7)))
* .scale("d3:minor:pentatonic")
* .s('sine')
* .dec(.3).room(.5)
* ._spectrum()
*/
let latestColor = {};
Pattern.prototype.spectrum = function (config = {}) {
let id = config.id ?? 1;
return this.analyze(id).draw(
(haps) => {
config.color = haps[0]?.value?.color || latestColor[id] || getTheme().foreground;
latestColor[id] = config.color;
drawSpectrum(analysers[id], config);
},
{ id },
);
};
Pattern.prototype.scope = Pattern.prototype.tscope;
const lastFrames = new Map();
function drawSpectrum(
analyser,
{ thickness = 3, speed = 1, min = -80, max = 0, ctx = getDrawContext(), id = 1, color } = {},
) {
ctx.lineWidth = thickness;
ctx.strokeStyle = color;
if (!analyser) {
// if analyser is undefined, draw straight line
// it may be undefined when no sound has been played yet
return;
}
const scrollSize = speed;
const dataArray = getAnalyzerData('frequency', id);
const canvas = ctx.canvas;
ctx.fillStyle = color;
const bufferSize = analyser.frequencyBinCount;
let imageData = lastFrames.get(id) || ctx.getImageData(0, 0, canvas.width, canvas.height);
lastFrames.set(id, imageData);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.putImageData(imageData, -scrollSize, 0);
let q = canvas.width - speed;
for (let i = 0; i < bufferSize; i++) {
const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1);
ctx.globalAlpha = normalized;
const next = (Math.log(i + 1) / Math.log(bufferSize)) * canvas.height;
const size = 2; //next - pos;
ctx.fillRect(q, canvas.height - next, scrollSize, size);
}
lastFrames.set(id, ctx.getImageData(0, 0, canvas.width, canvas.height));
}

View file

@ -7508,6 +7508,75 @@ exports[`runs examples > example "sometimesBy" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "spectrum" example index 0 1`] = `
[
"[ -5/24 ⇜ (0/1 → 1/8) | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ -2/15 ⇜ (0/1 → 1/5) | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ -1/120 ⇜ (0/1 → 1/5) ⇝ 13/40 | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 0/1 → 1/3 | note:D3 s:sine decay:0.3 room:0.5 ]",
"[ 1/8 → 11/24 | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ -1/120 ⇜ (1/5 → 13/40) | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 1/5 → 8/15 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 13/40 → 79/120 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 1/3 → 2/3 | note:C4 s:sine decay:0.3 room:0.5 ]",
"[ 11/24 → 19/24 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 8/15 → 13/15 | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ 79/120 → 119/120 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 2/3 → 1/1 | note:G3 s:sine decay:0.3 room:0.5 ]",
"[ (19/24 → 1/1) ⇝ 9/8 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ (13/15 → 1/1) ⇝ 6/5 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ (119/120 → 1/1) ⇝ 53/40 | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 19/24 ⇜ (1/1 → 9/8) | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 13/15 ⇜ (1/1 → 6/5) | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 119/120 ⇜ (1/1 → 6/5) ⇝ 53/40 | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 1/1 → 4/3 | note:F3 s:sine decay:0.3 room:0.5 ]",
"[ 9/8 → 35/24 | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ 119/120 ⇜ (6/5 → 53/40) | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 6/5 → 23/15 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ 53/40 → 199/120 | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 4/3 → 5/3 | note:D3 s:sine decay:0.3 room:0.5 ]",
"[ 35/24 → 43/24 | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ 23/15 → 28/15 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 199/120 → 239/120 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 5/3 → 2/1 | note:C4 s:sine decay:0.3 room:0.5 ]",
"[ (43/24 → 2/1) ⇝ 17/8 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ (28/15 → 2/1) ⇝ 11/5 | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ (239/120 → 2/1) ⇝ 93/40 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 43/24 ⇜ (2/1 → 17/8) | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 28/15 ⇜ (2/1 → 11/5) | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ 239/120 ⇜ (2/1 → 11/5) ⇝ 93/40 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 2/1 → 7/3 | note:A3 s:sine decay:0.3 room:0.5 ]",
"[ 17/8 → 59/24 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ 239/120 ⇜ (11/5 → 93/40) | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 11/5 → 38/15 | note:D5 s:sine decay:0.3 room:0.5 ]",
"[ 93/40 → 319/120 | note:D6 s:sine decay:0.3 room:0.5 ]",
"[ 7/3 → 8/3 | note:F3 s:sine decay:0.3 room:0.5 ]",
"[ 59/24 → 67/24 | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ 38/15 → 43/15 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ 319/120 → 359/120 | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 8/3 → 3/1 | note:D3 s:sine decay:0.3 room:0.5 ]",
"[ (67/24 → 3/1) ⇝ 25/8 | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ (43/15 → 3/1) ⇝ 16/5 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ (359/120 → 3/1) ⇝ 133/40 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 67/24 ⇜ (3/1 → 25/8) | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ 43/15 ⇜ (3/1 → 16/5) | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 359/120 ⇜ (3/1 → 16/5) ⇝ 133/40 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 3/1 → 10/3 | note:C4 s:sine decay:0.3 room:0.5 ]",
"[ 25/8 → 83/24 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 359/120 ⇜ (16/5 → 133/40) | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 16/5 → 53/15 | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ 133/40 → 439/120 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 10/3 → 11/3 | note:G3 s:sine decay:0.3 room:0.5 ]",
"[ 83/24 → 91/24 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 53/15 → 58/15 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 439/120 → 479/120 | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 11/3 → 4/1 | note:F3 s:sine decay:0.3 room:0.5 ]",
"[ (91/24 → 4/1) ⇝ 33/8 | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ (58/15 → 4/1) ⇝ 21/5 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ (479/120 → 4/1) ⇝ 173/40 | note:A5 s:sine decay:0.3 room:0.5 ]",
]
`;
exports[`runs examples > example "speed" example index 0 1`] = ` exports[`runs examples > example "speed" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | s:bd speed:1 ]", "[ 0/1 → 1/6 | s:bd speed:1 ]",

View file

@ -134,6 +134,9 @@ strudel.Pattern.prototype._pitchwheel = function () {
strudel.Pattern.prototype._pianoroll = function () { strudel.Pattern.prototype._pianoroll = function () {
return this; return this;
}; };
strudel.Pattern.prototype._spectrum = function () {
return this;
};
strudel.Pattern.prototype.markcss = function () { strudel.Pattern.prototype.markcss = function () {
return this; return this;
}; };

View file

@ -33,6 +33,7 @@
"@strudel/hydra": "workspace:*", "@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*", "@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*", "@strudel/mini": "workspace:*",
"@strudel/mqtt": "workspace:*",
"@strudel/osc": "workspace:*", "@strudel/osc": "workspace:*",
"@strudel/serial": "workspace:*", "@strudel/serial": "workspace:*",
"@strudel/soundfonts": "workspace:*", "@strudel/soundfonts": "workspace:*",

View file

@ -28,11 +28,10 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<div class="flex overflow-visible items-center grow" style="overflow:visible"> <div class="flex overflow-visible items-center grow" style="overflow:visible">
<div class="flex items-center text-2xl space-x-2"> <div class="flex items-center text-2xl space-x-2">
<h1 class="font-bold flex space-x-2 items-baseline text-xl"> <h1 class="font-bold flex space-x-2 items-baseline text-xl">
<span>🌀</span> <span class="block rotate-90 text-blue-500">꩜</span>
<div class="flex space-x-2 items-baseline"> <div class="flex space-x-2 items-baseline">
<span class="">strudel</span> <span class="">strudel</span>
<span class="text-sm">DOCS</span> <span class="text-sm font-medium">DOCS</span>
<a href={`${baseNoTrailing}/`} class="text-sm opacity-25">REPL</a>
</div> </div>
</h1> </h1>
</div> </div>

View file

@ -1,19 +1,20 @@
--- ---
title: MIDI & OSC title: MIDI, OSC & MQTT
layout: ../../layouts/MainLayout.astro layout: ../../layouts/MainLayout.astro
--- ---
import { MiniRepl } from '../../docs/MiniRepl'; import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc'; import { JsDoc } from '../../docs/JsDoc';
# MIDI and OSC # MIDI, OSC and MQTT
The default audio output of Strudel uses the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://github.com/tidalcycles/strudel/tree/main/packages/superdough).
It is also possible to use Strudel with MIDI and OSC / [SuperDirt](https://github.com/musikinformatik/SuperDirt/) instead.
# MIDI API It is also possible to pattern other things with Strudel, such as software and hardware synthesisers with MIDI, other software using Open Sound Control/OSC (including the [SuperDirt](https://github.com/musikinformatik/SuperDirt/) synthesiser commonly used with Strudel's sibling [TidalCycles](https://tidalcycles.org/)), or the MQTT 'internet of things' protocol.
Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi). # MIDI
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
## midi(outputName?) ## midi(outputName?)
@ -45,20 +46,22 @@ But you can also control cc messages separately like this:
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`} $: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
/> />
# OSC/SuperDirt API # OSC/SuperDirt/StrudelDirt
In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
Strudel also supports using [SuperDirt](https://github.com/musikinformatik/SuperDirt/) as a backend, although it requires some developer tooling to run.
There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is SuperDirt with some optimisations for working with Strudel. (A longer term aim is to merge these optimisations back into mainline SuperDirt)
## Prequisites ## Prequisites
Getting [SuperDirt](https://github.com/musikinformatik/SuperDirt/) to work with Strudel, you need to To get SuperDirt to work with Strudel, you need to
1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info. 1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info.
2. install [node.js](https://nodejs.org/en/) 2. install SuperDirt, or the [StrudelDirt](https://github.com/daslyfe/StrudelDirt) fork which is optimised for use with Strudel
3. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed) 3. install [node.js](https://nodejs.org/en/)
4. run `pnpm i` in the strudel directory 4. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
5. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider 5. run `pnpm i` in the strudel directory
6. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider
Now you're all set! Now you're all set!
@ -86,3 +89,67 @@ Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
<br /> <br />
But can we use Strudel [offline](/learn/pwa)? But can we use Strudel [offline](/learn/pwa)?
# MQTT
MQTT is a lightweight network protocol, designed for 'internet of things' devices. For use with strudel, you will
need access to an MQTT server known as a 'broker' configured to accept secure 'websocket' connections. You could
run one yourself (e.g. by running [mosquitto](https://mosquitto.org/)), although getting an SSL certificate that
your web browser will trust might be a bit tricky for those without systems administration experience.
Alternatively, you can use [a public broker](https://www.hivemq.com/mqtt/public-mqtt-broker/).
Strudel does not yet support receiving messages over MQTT, only sending them.
## Usage
The following example shows how to send a pattern to an MQTT broker:
<MiniRepl
client:only="react"
tune={`"hello world"
.mqtt(undefined, // username (undefined for open/public servers)
undefined, // password
'/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay)
)`}
/>
Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools:
```
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
hello
world
hello
world
...
```
Control patterns will be encoded as JSON, for example:
<MiniRepl
client:only="react"
tune={`sound("sax(3,8)").speed("2 3")
.mqtt(undefined, // username (undefined for open/public servers)
undefined, // password
'/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay)
)`}
/>
Will send messages like the following:
```
{"s":"sax","speed":2}
{"s":"sax","speed":2}
{"s":"sax","speed":3}
{"s":"sax","speed":2}
...
```
Libraries for receiving MQTT are available for many programming languages.

View file

@ -99,6 +99,10 @@ What follows is the API doc of all the options you can pass:
<JsDoc client:idle name="pitchwheel" h={0} /> <JsDoc client:idle name="pitchwheel" h={0} />
## Spectrum
<JsDoc client:idle name="spectrum" h={0} />
## markcss ## markcss
<JsDoc client:idle name="markcss" h={0} /> <JsDoc client:idle name="markcss" h={0} />

View file

@ -1,8 +1,4 @@
import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
import ArrowPathIcon from '@heroicons/react/20/solid/ArrowPathIcon';
import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { useSettings, setIsZen } from '../../settings.mjs'; import { useSettings, setIsZen } from '../../settings.mjs';
@ -21,7 +17,7 @@ export function Header({ context, embedded = false }) {
<header <header
id="header" id="header"
className={cx( className={cx(
'flex-none text-black z-[100] text-lg select-none h-14', 'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
!isZen && !isEmbedded && 'bg-lineHighlight', !isZen && !isEmbedded && 'bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between', isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
isEmbedded ? 'flex' : 'md:flex', isEmbedded ? 'flex' : 'md:flex',
@ -41,7 +37,7 @@ export function Header({ context, embedded = false }) {
className={cx( className={cx(
'mt-[1px]', 'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin', started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer', 'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4', isZen && 'fixed top-2 right-4',
)} )}
onClick={() => { onClick={() => {
@ -50,14 +46,14 @@ export function Header({ context, embedded = false }) {
} }
}} }}
> >
🌀 <span className="block rotate-90"></span>
</div> </div>
{!isZen && ( {!isZen && (
<div className={cx(started && !isCSSAnimationDisabled && 'animate-pulse', 'space-x-2')}> <div className="space-x-2">
<span className="">strudel</span> <span className="">strudel</span>
<span className="text-sm">REPL</span> <span className="text-sm font-medium">REPL</span>
{!isEmbedded && ( {!isEmbedded && isButtonRowHidden && (
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25"> <a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
DOCS DOCS
</a> </a>
)} )}
@ -66,7 +62,7 @@ export function Header({ context, embedded = false }) {
</h1> </h1>
</div> </div>
{!isZen && !isButtonRowHidden && ( {!isZen && !isButtonRowHidden && (
<div className="flex max-w-full overflow-auto text-foreground"> <div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
<button <button
onClick={handleTogglePlay} onClick={handleTogglePlay}
title={started ? 'stop' : 'play'} title={started ? 'stop' : 'play'}
@ -77,7 +73,7 @@ export function Header({ context, embedded = false }) {
)} )}
> >
{!pending ? ( {!pending ? (
<span className={cx('flex items-center space-x-1', isEmbedded ? '' : 'w-16')}> <span className={cx('flex items-center space-x-2')}>
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />} {started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>} {!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span> </span>
@ -94,8 +90,6 @@ export function Header({ context, embedded = false }) {
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50', !isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
)} )}
> >
{/* <CommandLineIcon className="w-6 h-6" /> */}
<ArrowPathIcon className="w-6 h-6" />
{!isEmbedded && <span>update</span>} {!isEmbedded && <span>update</span>}
</button> </button>
{!isEmbedded && ( {!isEmbedded && (
@ -104,7 +98,6 @@ export function Header({ context, embedded = false }) {
className="hover:opacity-50 p-2 flex items-center space-x-1" className="hover:opacity-50 p-2 flex items-center space-x-1"
onClick={handleShuffle} onClick={handleShuffle}
> >
<SparklesIcon className="w-6 h-6" />
<span> shuffle</span> <span> shuffle</span>
</button> </button>
)} )}
@ -117,7 +110,6 @@ export function Header({ context, embedded = false }) {
)} )}
onClick={handleShare} onClick={handleShare}
> >
<LinkIcon className="w-6 h-6" />
<span>share</span> <span>share</span>
</button> </button>
)} )}
@ -127,7 +119,6 @@ export function Header({ context, embedded = false }) {
href={`${baseNoTrailing}/workshop/getting-started/`} href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')} className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
> >
<AcademicCapIcon className="w-6 h-6" />
<span>learn</span> <span>learn</span>
</a> </a>
)} )}

View file

@ -1,18 +1,48 @@
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { useSettings } from '../../../settings.mjs';
export function ConsoleTab({ log }) { export function ConsoleTab() {
const [log, setLog] = useState([]);
const { fontFamily, fontSize } = useSettings();
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
return ( return (
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm py-2"> <div
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗ id="console-tab"
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
style={{ fontFamily, fontSize }}
>
`}</pre>
{log.map((l, i) => { {log.map((l, i) => {
const message = linkify(l.message); const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
return ( return (
<div key={l.id} className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}> <div
key={l.id}
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} /> <span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''} {l.count ? ` (${l.count})` : ''}
</div> </div>
@ -42,3 +72,7 @@ function linkify(inputText) {
return replacedText; return replacedText;
} }
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}

View file

@ -1,8 +1,4 @@
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs'; import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab'; import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab'; import { FilesTab } from './FilesTab';
@ -119,33 +115,11 @@ function PanelNav({ children, className, settings, ...props }) {
} }
function PanelContent({ context, tab }) { function PanelContent({ context, tab }) {
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
switch (tab) { switch (tab) {
case tabNames.patterns: case tabNames.patterns:
return <PatternsTab context={context} />; return <PatternsTab context={context} />;
case tabNames.console: case tabNames.console:
return <ConsoleTab log={log} />; return <ConsoleTab />;
case tabNames.sounds: case tabNames.sounds:
return <SoundsTab />; return <SoundsTab />;
case tabNames.reference: case tabNames.reference:
@ -236,7 +210,3 @@ function CloseButton({ onClick }) {
</button> </button>
); );
} }
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}

View file

@ -58,8 +58,8 @@ export function Reference() {
<div className="prose dark:prose-invert min-w-full px-1 "> <div className="prose dark:prose-invert min-w-full px-1 ">
<h2>API Reference</h2> <h2>API Reference</h2>
<p> <p>
This is the long list functions you can use! Remember that you don't need to remember all of those and that This is the long list of functions you can use. Remember that you don't need to remember all of those and
you can already make music with a small set of functions! that you can already make music with a small set of functions!
</p> </p>
{visibleFunctions.map((entry, i) => ( {visibleFunctions.map((entry, i) => (
<section key={i}> <section key={i}>

View file

@ -6,9 +6,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) { export function WelcomeTab({ context }) {
return ( return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 "> <div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<h3> <h3> welcome</h3>
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
</h3>
<p> <p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started: pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
@ -30,7 +28,7 @@ export function WelcomeTab({ context }) {
</a>{' '} </a>{' '}
to ask any questions, give feedback or just say hello. to ask any questions, give feedback or just say hello.
</p> </p>
<h3>about</h3> <h3> about</h3>
<p> <p>
strudel is a JavaScript version of{' '} strudel is a JavaScript version of{' '}
<a href="https://tidalcycles.org/" target="_blank"> <a href="https://tidalcycles.org/" target="_blank">

View file

@ -81,6 +81,7 @@ export function loadModules() {
import('@strudel/soundfonts'), import('@strudel/soundfonts'),
import('@strudel/csound'), import('@strudel/csound'),
import('@strudel/tidal'), import('@strudel/tidal'),
import('@strudel/mqtt'),
]; ];
if (isTauri()) { if (isTauri()) {
modules = modules.concat([ modules = modules.concat([