merge main
This commit is contained in:
commit
1da80db048
30 changed files with 766 additions and 319 deletions
14
README.md
14
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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
214
packages/core/pick.mjs
Normal 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();
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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
3
packages/mqtt/README.md
Normal 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
87
packages/mqtt/mqtt.mjs
Normal 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 });
|
||||||
|
});
|
||||||
|
};
|
||||||
38
packages/mqtt/package.json
Normal file
38
packages/mqtt/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/mqtt/vite.config.js
Normal file
19
packages/mqtt/vite.config.js
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
69
packages/webaudio/spectrum.mjs
Normal file
69
packages/webaudio/spectrum.mjs
Normal 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));
|
||||||
|
}
|
||||||
|
|
@ -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 ]",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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:*",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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([
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue