mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
Merge branch 'main' of https://github.com/tidalcycles/strudel into add-program-change
This commit is contained in:
commit
bfb4eee122
@ -138,6 +138,18 @@ pnpm --filter "./packages/**" publish --access public
|
||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||
Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages.
|
||||
|
||||
|
||||
## useful commands
|
||||
```sh
|
||||
#regenerate the test snapshots (ex: when updating or creating new pattern functions)
|
||||
pnpm snapshot
|
||||
|
||||
#start the OSC server
|
||||
pnpm run osc
|
||||
|
||||
#build the standalone version
|
||||
pnpm tauri build
|
||||
```
|
||||
## Have Fun
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
pnpm dev
|
||||
```
|
||||
1. Install [Node.js](https://nodejs.org/)
|
||||
2. Install [pnpm](https://pnpm.io/installation)
|
||||
3. Install dependencies by running the following command:
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
4. Run the development server:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
|
||||
|
||||
@ -133,6 +133,7 @@ export class StrudelMirror {
|
||||
autodraw,
|
||||
prebake,
|
||||
bgFill = true,
|
||||
solo = true,
|
||||
...replOptions
|
||||
} = options;
|
||||
this.code = initialCode;
|
||||
@ -143,6 +144,7 @@ export class StrudelMirror {
|
||||
this.drawContext = drawContext;
|
||||
this.onDraw = onDraw || this.draw;
|
||||
this.id = id || s4();
|
||||
this.solo = solo;
|
||||
|
||||
this.drawer = new Drawer((haps, time, _, painters) => {
|
||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||
@ -159,12 +161,14 @@ export class StrudelMirror {
|
||||
replOptions?.onToggle?.(started);
|
||||
if (started) {
|
||||
this.drawer.start(this.repl.scheduler);
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('start-repl', {
|
||||
detail: this.id,
|
||||
}),
|
||||
);
|
||||
if (this.solo) {
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('start-repl', {
|
||||
detail: this.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.drawer.stop();
|
||||
updateMiniLocations(this.editor, []);
|
||||
@ -219,7 +223,7 @@ export class StrudelMirror {
|
||||
|
||||
// stop this repl when another repl is started
|
||||
this.onStartRepl = (e) => {
|
||||
if (e.detail !== this.id) {
|
||||
if (this.solo && e.detail !== this.id) {
|
||||
this.stop();
|
||||
}
|
||||
};
|
||||
|
||||
@ -133,3 +133,10 @@ registerWidget('_pitchwheel', (id, options = {}, pat) => {
|
||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||
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
|
||||
const deadline = targetTime - phase;
|
||||
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) {
|
||||
|
||||
@ -14,6 +14,7 @@ export * from './controls.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './pick.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
|
||||
@ -1023,6 +1023,7 @@ function _composeOp(a, b, func) {
|
||||
div: [numeralArgs((a, b) => a / b)],
|
||||
mod: [numeralArgs(_mod)],
|
||||
pow: [numeralArgs(Math.pow)],
|
||||
log2: [numeralArgs(Math.log2)],
|
||||
band: [numeralArgs((a, b) => a & b)],
|
||||
bor: [numeralArgs((a, b) => a | b)],
|
||||
bxor: [numeralArgs((a, b) => a ^ b)],
|
||||
@ -1491,6 +1492,14 @@ function _sequenceCount(x) {
|
||||
export const mask = curry((a, b) => reify(b).mask(a));
|
||||
export const struct = curry((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||
export const withValue = curry((a, b) => reify(b).withValue(a));
|
||||
|
||||
export const bind = curry((a, b) => reify(b).bind(a));
|
||||
export const innerBind = curry((a, b) => reify(b).innerBind(a));
|
||||
export const outerBind = curry((a, b) => reify(b).outerBind(a));
|
||||
export const squeezeBind = curry((a, b) => reify(b).squeezeBind(a));
|
||||
export const stepBind = curry((a, b) => reify(b).stepBind(a));
|
||||
export const polyBind = curry((a, b) => reify(b).polyBind(a));
|
||||
|
||||
// operators
|
||||
export const set = curry((a, b) => reify(b).set(a));
|
||||
@ -2539,6 +2548,10 @@ Pattern.prototype.stepJoin = function () {
|
||||
return new Pattern(q, first_t);
|
||||
};
|
||||
|
||||
Pattern.prototype.stepBind = function (func) {
|
||||
return this.fmap(func).stepJoin();
|
||||
};
|
||||
|
||||
export function _retime(timedHaps) {
|
||||
const occupied_perc = timedHaps.filter((t, pat) => pat.hasTactus).reduce((a, b) => a.add(b), Fraction(0));
|
||||
const occupied_tactus = removeUndefineds(timedHaps.map((t, pat) => pat.tactus)).reduce(
|
||||
@ -2593,7 +2606,7 @@ export const steps = register('steps', function (targetTactus, pat) {
|
||||
// avoid divide by zero..
|
||||
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) {
|
||||
@ -2801,7 +2814,7 @@ export const s_sub = stepRegister('s_sub', function (i, pat) {
|
||||
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);
|
||||
});
|
||||
|
||||
@ -2882,6 +2895,13 @@ export const s_tour = function (pat, ...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
|
||||
// objects
|
||||
@ -3093,3 +3113,25 @@ export let xfade = (a, pos, b) => {
|
||||
Pattern.prototype.xfade = function (pos, b) {
|
||||
return xfade(this, pos, b);
|
||||
};
|
||||
|
||||
/**
|
||||
* creates a structure pattern from divisions of a cycle
|
||||
* especially useful for creating rhythms
|
||||
* @name beat
|
||||
* @example
|
||||
* s("bd").beat("0:7:10", 16)
|
||||
* @example
|
||||
* s("sd").beat("4:12", 16)
|
||||
*/
|
||||
const __beat = (join) => (t, div, pat) => {
|
||||
t = Fraction(t).mod(div);
|
||||
div = Fraction(div);
|
||||
const b = t.div(div);
|
||||
const e = t.add(1).div(div);
|
||||
return join(pat.fmap((x) => pure(x)._compress(b, e)));
|
||||
};
|
||||
|
||||
export const { beat } = register(
|
||||
['beat'],
|
||||
__beat((x) => x.innerJoin()),
|
||||
);
|
||||
|
||||
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 anonymousIndex = 0;
|
||||
let allTransform;
|
||||
let eachTransform;
|
||||
|
||||
const hush = function () {
|
||||
pPatterns = {};
|
||||
anonymousIndex = 0;
|
||||
allTransform = undefined;
|
||||
eachTransform = undefined;
|
||||
return silence;
|
||||
};
|
||||
|
||||
@ -84,15 +86,42 @@ export function repl({
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
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) {
|
||||
allTransform = transform;
|
||||
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
|
||||
const injectPatternMethods = () => {
|
||||
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
|
||||
return silence;
|
||||
}
|
||||
@ -131,6 +160,7 @@ export function repl({
|
||||
});
|
||||
return evalScope({
|
||||
all,
|
||||
each,
|
||||
hush,
|
||||
cpm,
|
||||
setCps,
|
||||
@ -152,7 +182,14 @@ export function repl({
|
||||
shouldHush && hush();
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
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) {
|
||||
pattern = allTransform(pattern);
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
/*
|
||||
signal.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
|
||||
signal.mjs - continuous 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 { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id, _mod, clamp, objectMap } from './util.mjs';
|
||||
|
||||
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
||||
|
||||
export function steady(value) {
|
||||
// A continuous value
|
||||
@ -159,6 +160,37 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
*/
|
||||
export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||
|
||||
/**
|
||||
* Creates a pattern from a binary number.
|
||||
*
|
||||
* @name binary
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @example
|
||||
* "hh".s().struct(binary(5))
|
||||
* // "hh".s().struct("1 0 1")
|
||||
*/
|
||||
export const binary = (n) => {
|
||||
const nBits = reify(n).log2(0).floor().add(1);
|
||||
return binaryN(n, nBits);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a pattern from a binary number, padded to n bits long.
|
||||
*
|
||||
* @name binaryN
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @param {number} nBits - pattern length, defaults to 16
|
||||
* @example
|
||||
* "hh".s().struct(binaryN(55532, 16))
|
||||
* // "hh".s().struct("1 1 0 1 1 0 0 0 1 1 1 0 1 1 0 0")
|
||||
*/
|
||||
export const binaryN = (n, nBits = 16) => {
|
||||
nBits = reify(nBits);
|
||||
// Shift and mask, putting msb on the right-side
|
||||
const bitPos = run(nBits).mul(-1).add(nBits.sub(1));
|
||||
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
|
||||
};
|
||||
|
||||
export const randrun = (n) => {
|
||||
return signal((t) => {
|
||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||
@ -253,211 +285,6 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
*/
|
||||
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) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
@ -596,8 +423,11 @@ export const perlinWith = (pat) => {
|
||||
*/
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
|
||||
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||
export const degradeByWith = register(
|
||||
'degradeByWith',
|
||||
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
@ -614,9 +444,14 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||
* @example
|
||||
* s("[hh?0.2]*8")
|
||||
*/
|
||||
export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(rand, x);
|
||||
});
|
||||
export const degradeBy = register(
|
||||
'degradeBy',
|
||||
function (x, pat) {
|
||||
return pat._degradeByWith(rand, x);
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
@ -630,7 +465,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
* @example
|
||||
* 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.
|
||||
@ -650,12 +485,17 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||
* x => x.undegradeBy(0.8).pan(1)
|
||||
* )
|
||||
*/
|
||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
x,
|
||||
);
|
||||
});
|
||||
export const undegradeBy = register(
|
||||
'undegradeBy',
|
||||
function (x, pat) {
|
||||
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)`
|
||||
@ -672,7 +512,7 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||
* 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);
|
||||
|
||||
/**
|
||||
*
|
||||
@ -690,7 +530,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||
|
||||
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||
return reify(patx)
|
||||
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
||||
.fmap((x) => stack(pat._degradeBy(x), func(pat)._undegradeBy(1 - x)))
|
||||
.innerJoin();
|
||||
});
|
||||
|
||||
@ -831,3 +671,48 @@ export const never = register('never', function (_, pat) {
|
||||
export const always = register('always', function (func, pat) {
|
||||
return func(pat);
|
||||
});
|
||||
|
||||
//keyname: string | Array<string>
|
||||
//keyname reference: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
||||
export function _keyDown(keyname) {
|
||||
if (Array.isArray(keyname) === false) {
|
||||
keyname = [keyname];
|
||||
}
|
||||
const keyState = getCurrentKeyboardState();
|
||||
return keyname.every((x) => {
|
||||
const keyName = keyAlias.get(x) ?? x;
|
||||
return keyState[keyName];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Do something on a keypress, or array of keypresses
|
||||
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||
*
|
||||
* @name whenKey
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd(5,8)").whenKey("Control:j", x => x.segment(16).color("red")).whenKey("Control:i", x => x.fast(2).color("blue"))
|
||||
*/
|
||||
|
||||
export const whenKey = register('whenKey', function (input, func, pat) {
|
||||
return pat.when(_keyDown(input), func);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* returns true when a key or array of keys is held
|
||||
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||
*
|
||||
* @name keyDown
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* keyDown("Control:j").pick([s("bd(5,8)"), s("cp(3,8)")])
|
||||
*/
|
||||
|
||||
export const keyDown = register('keyDown', function (pat) {
|
||||
return pat.fmap(_keyDown);
|
||||
});
|
||||
|
||||
@ -46,12 +46,14 @@ import {
|
||||
rev,
|
||||
time,
|
||||
run,
|
||||
binaryN,
|
||||
pick,
|
||||
stackLeft,
|
||||
stackRight,
|
||||
stackCentre,
|
||||
s_cat,
|
||||
calculateTactus,
|
||||
sometimes,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
@ -958,6 +960,18 @@ describe('Pattern', () => {
|
||||
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('binaryN', () => {
|
||||
it('Can make a binary pattern from a decimal', () => {
|
||||
expect(binaryN(55532).firstCycle()).toStrictEqual(
|
||||
sequence(1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can make a binary pattern from patterned inputs', () => {
|
||||
expect(binaryN(pure(0x1337), pure(14)).firstCycle()).toStrictEqual(
|
||||
sequence(0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('ribbon', () => {
|
||||
it('Can ribbon', () => {
|
||||
expect(cat(0, 1, 2, 3, 4, 5, 6, 7).ribbon(2, 4).fast(4).firstCycle()).toStrictEqual(
|
||||
@ -1253,4 +1267,13 @@ describe('Pattern', () => {
|
||||
expect(s('bev').chop(8).loopAt(2).tactus).toStrictEqual(Fraction(4));
|
||||
});
|
||||
});
|
||||
describe('sometimes', () => {
|
||||
it('works with constant functions', () => {
|
||||
expect(
|
||||
pure('a')
|
||||
.sometimes((x) => pure('b'))
|
||||
.fast(16).firstCycleValues.length,
|
||||
).toStrictEqual(16);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -434,6 +434,38 @@ function getUnixTimeSeconds() {
|
||||
return Date.now() * 0.001;
|
||||
}
|
||||
|
||||
export const keyAlias = new Map([
|
||||
['control', 'Control'],
|
||||
['ctrl', 'Control'],
|
||||
['alt', 'Alt'],
|
||||
['shift', 'Shift'],
|
||||
['down', 'ArrowDown'],
|
||||
['up', 'ArrowUp'],
|
||||
['left', 'ArrowLeft'],
|
||||
['right', 'ArrowRight'],
|
||||
]);
|
||||
let keyState;
|
||||
|
||||
export function getCurrentKeyboardState() {
|
||||
if (keyState == null) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
keyState = {};
|
||||
// Listen for the keydown event to mark the key as pressed
|
||||
window.addEventListener('keydown', (event) => {
|
||||
keyState[event.key] = true; // Mark the key as pressed
|
||||
});
|
||||
|
||||
// Listen for the keyup event to mark the key as released
|
||||
window.addEventListener('keyup', (event) => {
|
||||
keyState[event.key] = false; // Mark the key as released
|
||||
});
|
||||
}
|
||||
|
||||
return { ...keyState }; // Return a shallow copy of the key state object
|
||||
}
|
||||
|
||||
// Floating point versions, see Fraction for rational versions
|
||||
// // greatest common divisor
|
||||
// export const gcd = function (x, y, ...z) {
|
||||
|
||||
@ -26,7 +26,7 @@ export const getDrawContext = (id = 'test-canvas', options) => {
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
return canvas.getContext(contextType);
|
||||
return canvas.getContext(contextType, { willReadFrequently: true });
|
||||
};
|
||||
|
||||
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'];
|
||||
settings = codemirrorSettings.get();
|
||||
editor = null;
|
||||
sync = false;
|
||||
solo = true;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@ -49,6 +51,8 @@ if (typeof HTMLElement !== 'undefined') {
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
},
|
||||
solo: this.solo,
|
||||
sync: this.sync,
|
||||
});
|
||||
// init settings
|
||||
this.editor.updateSettings(this.settings);
|
||||
|
||||
@ -270,11 +270,12 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
|
||||
export let analysers = {},
|
||||
analysersData = {};
|
||||
|
||||
export function getAnalyserById(id, fftSize = 1024) {
|
||||
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
|
||||
if (!analysers[id]) {
|
||||
// make sure this doesn't happen too often as it piles up garbage
|
||||
const analyserNode = getAudioContext().createAnalyser();
|
||||
analyserNode.fftSize = fftSize;
|
||||
analyserNode.smoothingTimeConstant = smoothingTimeConstant;
|
||||
// getDestination().connect(analyserNode);
|
||||
analysers[id] = analyserNode;
|
||||
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 './scope.mjs';
|
||||
export * from './spectrum.mjs';
|
||||
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));
|
||||
}
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -331,6 +331,19 @@ importers:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3(@types/node@22.7.6)(@vitest/ui@2.1.3)(terser@5.36.0)
|
||||
|
||||
packages/mqtt:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
paho-mqtt:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^5.0.10
|
||||
version: 5.4.9(@types/node@22.7.6)(terser@5.36.0)
|
||||
|
||||
packages/osc:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@ -623,6 +636,9 @@ importers:
|
||||
'@strudel/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/mini
|
||||
'@strudel/mqtt':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/mqtt
|
||||
'@strudel/osc':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/osc
|
||||
@ -5991,6 +6007,9 @@ packages:
|
||||
engines: {node: ^16.14.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
|
||||
paho-mqtt@1.1.0:
|
||||
resolution: {integrity: sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@ -7740,6 +7759,7 @@ packages:
|
||||
|
||||
workbox-google-analytics@7.0.0:
|
||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||
|
||||
workbox-navigation-preload@7.0.0:
|
||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||
@ -14686,6 +14706,8 @@ snapshots:
|
||||
- bluebird
|
||||
- supports-color
|
||||
|
||||
paho-mqtt@1.1.0: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
@ -954,6 +954,24 @@ exports[`runs examples > example "bank" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | s:bd ]",
|
||||
"[ 1/1 → 17/16 | s:bd ]",
|
||||
"[ 2/1 → 33/16 | s:bd ]",
|
||||
"[ 3/1 → 49/16 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "beat" example index 1 1`] = `
|
||||
[
|
||||
"[ 1/48 → 1/12 | s:sd ]",
|
||||
"[ 49/48 → 13/12 | s:sd ]",
|
||||
"[ 97/48 → 25/12 | s:sd ]",
|
||||
"[ 145/48 → 37/12 | s:sd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "begin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:rave begin:0 ]",
|
||||
@ -967,6 +985,60 @@ exports[`runs examples > example "begin" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "binary" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | s:hh ]",
|
||||
"[ 2/3 → 1/1 | s:hh ]",
|
||||
"[ 1/1 → 4/3 | s:hh ]",
|
||||
"[ 5/3 → 2/1 | s:hh ]",
|
||||
"[ 2/1 → 7/3 | s:hh ]",
|
||||
"[ 8/3 → 3/1 | s:hh ]",
|
||||
"[ 3/1 → 10/3 | s:hh ]",
|
||||
"[ 11/3 → 4/1 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "binaryN" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | s:hh ]",
|
||||
"[ 1/16 → 1/8 | s:hh ]",
|
||||
"[ 3/16 → 1/4 | s:hh ]",
|
||||
"[ 1/4 → 5/16 | s:hh ]",
|
||||
"[ 1/2 → 9/16 | s:hh ]",
|
||||
"[ 9/16 → 5/8 | s:hh ]",
|
||||
"[ 5/8 → 11/16 | s:hh ]",
|
||||
"[ 3/4 → 13/16 | s:hh ]",
|
||||
"[ 13/16 → 7/8 | s:hh ]",
|
||||
"[ 1/1 → 17/16 | s:hh ]",
|
||||
"[ 17/16 → 9/8 | s:hh ]",
|
||||
"[ 19/16 → 5/4 | s:hh ]",
|
||||
"[ 5/4 → 21/16 | s:hh ]",
|
||||
"[ 3/2 → 25/16 | s:hh ]",
|
||||
"[ 25/16 → 13/8 | s:hh ]",
|
||||
"[ 13/8 → 27/16 | s:hh ]",
|
||||
"[ 7/4 → 29/16 | s:hh ]",
|
||||
"[ 29/16 → 15/8 | s:hh ]",
|
||||
"[ 2/1 → 33/16 | s:hh ]",
|
||||
"[ 33/16 → 17/8 | s:hh ]",
|
||||
"[ 35/16 → 9/4 | s:hh ]",
|
||||
"[ 9/4 → 37/16 | s:hh ]",
|
||||
"[ 5/2 → 41/16 | s:hh ]",
|
||||
"[ 41/16 → 21/8 | s:hh ]",
|
||||
"[ 21/8 → 43/16 | s:hh ]",
|
||||
"[ 11/4 → 45/16 | s:hh ]",
|
||||
"[ 45/16 → 23/8 | s:hh ]",
|
||||
"[ 3/1 → 49/16 | s:hh ]",
|
||||
"[ 49/16 → 25/8 | s:hh ]",
|
||||
"[ 51/16 → 13/4 | s:hh ]",
|
||||
"[ 13/4 → 53/16 | s:hh ]",
|
||||
"[ 7/2 → 57/16 | s:hh ]",
|
||||
"[ 57/16 → 29/8 | s:hh ]",
|
||||
"[ 29/8 → 59/16 | s:hh ]",
|
||||
"[ 15/4 → 61/16 | s:hh ]",
|
||||
"[ 61/16 → 31/8 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "bite" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:Bb3 ]",
|
||||
@ -4065,6 +4137,8 @@ exports[`runs examples > example "juxBy" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "keyDown" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "lastOf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c3 ]",
|
||||
@ -7508,6 +7582,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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | s:bd speed:1 ]",
|
||||
@ -8763,6 +8906,8 @@ exports[`runs examples > example "when" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "whenKey" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "withValue" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | 10 ]",
|
||||
|
||||
@ -8,6 +8,7 @@ exports[`renders tunes > tune: amensister 1`] = `
|
||||
"[ 0/1 → 1/4 | n:0 s:amencutup room:0.5 ]",
|
||||
"[ 1/16 → 1/8 | s:breath room:1 shape:0.6 begin:0.875 end:0.9375 ]",
|
||||
"[ 1/8 → 3/16 | s:breath room:1 shape:0.6 begin:0.8125 end:0.875 ]",
|
||||
"[ 1/8 → 1/4 | n:0 s:amencutup room:0.5 ]",
|
||||
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
|
||||
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
|
||||
"[ 3/16 → 1/4 | s:breath room:1 shape:0.6 begin:0.75 end:0.8125 ]",
|
||||
@ -17,22 +18,20 @@ exports[`renders tunes > tune: amensister 1`] = `
|
||||
"[ 1/4 → 3/8 | note:A1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.7878869297153 ]",
|
||||
"[ 5/16 → 3/8 | s:breath room:1 shape:0.6 begin:0.625 end:0.6875 ]",
|
||||
"[ 3/8 → 7/16 | s:breath room:1 shape:0.6 begin:0.5625 end:0.625 ]",
|
||||
"[ 3/8 → 1/2 | n:1 s:amencutup room:0.5 ]",
|
||||
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
|
||||
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
|
||||
"[ 7/16 → 1/2 | s:breath room:1 shape:0.6 begin:0.5 end:0.5625 ]",
|
||||
"[ 1/2 → 9/16 | s:breath room:1 shape:0.6 begin:0.4375 end:0.5 ]",
|
||||
"[ 1/2 → 5/8 | n:2 s:amencutup room:0.5 ]",
|
||||
"[ 1/2 → 3/4 | n:2 s:amencutup room:0.5 ]",
|
||||
"[ 9/16 → 5/8 | s:breath room:1 shape:0.6 begin:0.375 end:0.4375 ]",
|
||||
"[ 5/8 → 11/16 | s:breath room:1 shape:0.6 begin:0.3125 end:0.375 ]",
|
||||
"[ 11/16 → 3/4 | s:breath room:1 shape:0.6 begin:0.25 end:0.3125 ]",
|
||||
"[ 3/4 → 13/16 | s:breath room:1 shape:0.6 begin:0.1875 end:0.25 ]",
|
||||
"[ 3/4 → 7/8 | n:3 s:amencutup room:0.5 ]",
|
||||
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
|
||||
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
|
||||
"[ 13/16 → 7/8 | s:breath room:1 shape:0.6 begin:0.125 end:0.1875 ]",
|
||||
"[ 7/8 → 15/16 | s:breath room:1 shape:0.6 begin:0.0625 end:0.125 ]",
|
||||
"[ 7/8 → 1/1 | n:3 s:amencutup room:0.5 ]",
|
||||
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
|
||||
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
|
||||
"[ 15/16 → 1/1 | s:breath room:1 shape:0.6 begin:0 end:0.0625 ]",
|
||||
@ -6906,10 +6905,10 @@ exports[`renders tunes > tune: flatrave 1`] = `
|
||||
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/8 → 1/4 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 3/8 → 1/2 | s:hh n:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/2 → 5/8 | s:hh n:1 speed:0.5 delay:0.5 end:0.020048626493108724 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/2 → 5/8 | s:hh n:1 speed:0.5 delay:0.5 end:0.020048626493108724 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]",
|
||||
"[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]",
|
||||
@ -6917,7 +6916,6 @@ exports[`renders tunes > tune: flatrave 1`] = `
|
||||
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 5/8 → 3/4 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
"[ 3/4 → 7/8 | s:hh n:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
|
||||
"[ 7/8 → 1/1 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@ -134,6 +134,9 @@ strudel.Pattern.prototype._pitchwheel = function () {
|
||||
strudel.Pattern.prototype._pianoroll = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype._spectrum = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype.markcss = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/midi": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/mqtt": "workspace:*",
|
||||
"@strudel/osc": "workspace:*",
|
||||
"@strudel/serial": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
|
||||
@ -82,18 +82,7 @@ To modify a parameter value, you can either:
|
||||
|
||||
<MiniRepl client:only="react" tune={`note("50 60 70").room(.1).add(note("<0 1 2>")).log()`} />
|
||||
|
||||
- Modify _all_ numeral params:
|
||||
|
||||
<MiniRepl client:only="react" tune={`note("50 60 70").room(.1).add("<0 1 2>").log()`} />
|
||||
|
||||
Which of these 3 ways to use strongly depends on the context!
|
||||
Note that the order of chaining param functions also matters!
|
||||
In the last example, the `room` value would not have changed if it was applied later:
|
||||
|
||||
<MiniRepl client:only="react" tune={`note("50 60 70").add("<0 1 2>").room(.1).log()`} />
|
||||
|
||||
This shows how the execution of the chained functions goes from left to right.
|
||||
In this case, the `.add` will only modify what's on the left side.
|
||||
Remember the execution of the chained functions goes from left to right.
|
||||
|
||||
# Operators
|
||||
|
||||
|
||||
@ -55,6 +55,14 @@ These are the equivalents used by the Mini Notation:
|
||||
|
||||
## run
|
||||
|
||||
<JsDoc client:idle name="run" h={0} punchcard />
|
||||
<JsDoc client:idle name="run" h={0} />
|
||||
|
||||
## binary
|
||||
|
||||
<JsDoc client:idle name="binary" h={0} />
|
||||
|
||||
## binaryN
|
||||
|
||||
<JsDoc client:idle name="binaryN" h={0} />
|
||||
|
||||
After Pattern Constructors, let's see what [Time Modifiers](/learn/time-modifiers) are available.
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
---
|
||||
title: MIDI & OSC
|
||||
title: MIDI, OSC & MQTT
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
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).
|
||||
It is also possible to use Strudel with MIDI and OSC / [SuperDirt](https://github.com/musikinformatik/SuperDirt/) instead.
|
||||
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).
|
||||
|
||||
# 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?)
|
||||
|
||||
@ -84,13 +85,14 @@ There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is Sup
|
||||
|
||||
## 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.
|
||||
2. install [node.js](https://nodejs.org/en/)
|
||||
3. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
|
||||
4. run `pnpm i` in the strudel directory
|
||||
5. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider
|
||||
2. install SuperDirt, or the [StrudelDirt](https://github.com/daslyfe/StrudelDirt) fork which is optimised for use with Strudel
|
||||
3. install [node.js](https://nodejs.org/en/)
|
||||
4. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
|
||||
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!
|
||||
|
||||
@ -118,3 +120,67 @@ Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
|
||||
<br />
|
||||
|
||||
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} />
|
||||
|
||||
## Spectrum
|
||||
|
||||
<JsDoc client:idle name="spectrum" h={0} />
|
||||
|
||||
## markcss
|
||||
|
||||
<JsDoc client:idle name="markcss" h={0} />
|
||||
|
||||
@ -58,8 +58,8 @@ export function Reference() {
|
||||
<div className="prose dark:prose-invert min-w-full px-1 ">
|
||||
<h2>API Reference</h2>
|
||||
<p>
|
||||
This is the long list functions you can use! Remember that you don't need to remember all of those and that
|
||||
you can already make music with a small set of functions!
|
||||
This is the long list of functions you can use. Remember that you don't need to remember all of those and
|
||||
that you can already make music with a small set of functions!
|
||||
</p>
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<section key={i}>
|
||||
|
||||
@ -81,6 +81,7 @@ export function loadModules() {
|
||||
import('@strudel/soundfonts'),
|
||||
import('@strudel/csound'),
|
||||
import('@strudel/tidal'),
|
||||
import('@strudel/mqtt'),
|
||||
];
|
||||
if (isTauri()) {
|
||||
modules = modules.concat([
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user