merge main

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

View file

@ -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();
}
};

View file

@ -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 });
});

View file

@ -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) {

View file

@ -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';

View file

@ -2593,7 +2593,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 +2801,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 +2882,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

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

@ -0,0 +1,214 @@
/*
pick.mjs - methods that use one pattern to pick events from other patterns.
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, reify, silence, register } from './pattern.mjs';
import { _mod, clamp, objectMap } from './util.mjs';
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a@2 b@2 c@2 d@2>".pickRestart({
a: n("0 1 2 0"),
b: n("2 3 4 ~"),
c: n("[4 5] [4 3] 2 0"),
d: n("0 -3 0 ~")
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};

View file

@ -63,11 +63,13 @@ export function repl({
let pPatterns = {};
let 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);

View file

@ -1,6 +1,6 @@
/*
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/>.
*/
@ -8,7 +8,7 @@ import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.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) {
// 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();
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) {
@ -597,8 +392,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,
);
/**
@ -615,9 +413,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,
);
/**
*
@ -631,7 +434,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.
@ -651,12 +454,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)`
@ -673,7 +481,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);
/**
*

View file

@ -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
View file

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

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

@ -0,0 +1,87 @@
/*
mqtt.mjs - for patterning the internet of things from strudel
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/serial/serial.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, isPattern } from '@strudel/core';
import Paho from 'paho-mqtt';
const connections = {};
// Handle connection loss
function onConnectionLost(responseObject) {
if (responseObject.errorCode !== 0) {
console.error(' mqtt connection lost: ', responseObject.errorMessage);
}
}
// Handle received messages
function onMessageArrived(message) {
console.log('incoming mqtt message: ', message.payloadString); // prettier-ignore
}
function onFailure(err) {
console.error('Connection failed: ', err);
}
Pattern.prototype.mqtt = function (
username = undefined,
password = undefined,
topic = undefined,
host = 'wss://localhost:8883/',
client = undefined,
latency = 0,
) {
const key = host + '-' + client;
let connected = false;
if (!client) {
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
}
function onConnect() {
console.log('Connected to mqtt broker');
connected = true;
}
let cx;
if (connections[key]) {
cx = connections[key];
} else {
cx = new Paho.Client(host, client);
cx.onConnectionLost = onConnectionLost;
cx.onMessageArrived = onMessageArrived;
const props = {
onSuccess: onConnect,
onFailure: onFailure,
useSSL: true,
};
if (username) {
props.userName = username;
props.password = password;
}
cx.connect(props);
}
return this.withHap((hap) => {
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
if (!connected) {
return;
}
let message = '';
if (typeof hap.value === 'object') {
message = JSON.stringify(hap.value);
} else {
message = hap.value;
}
message = new Paho.Message(message);
message.destinationName = topic;
const offset = (targetTime - currentTime + latency) * 1000;
window.setTimeout(function () {
cx.send(message);
}, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
});
};

View file

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

View file

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

View file

@ -10,6 +10,8 @@ if (typeof HTMLElement !== 'undefined') {
static observedAttributes = ['code'];
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);

View file

@ -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);

View file

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

View file

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