mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 05:05:26 -04:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b8d83c548 | |||
| 345ef2276f | |||
| 2c880e2596 | |||
| f61916fffa | |||
| 00b0341839 | |||
| d5ab391a98 | |||
| d2a9b9b750 | |||
| 2348023b22 | |||
| 6e5d8eed19 | |||
| c33c8b5469 | |||
| 7b6074ccef | |||
| 55c226cda6 | |||
| c5de21ef61 | |||
| 36374fb3fb | |||
| 570c7a0d6b | |||
| c3be21e6e5 | |||
| 7410ac5cec | |||
| f5127147de | |||
| 1d144faed7 | |||
| 7b914e7f22 | |||
| 052addd67e | |||
| 3d162536aa | |||
| 2443aad01b | |||
| 08f6c1ad5e | |||
| 974376d404 | |||
| 1da80db048 | |||
| 42519581c1 | |||
| 1e3bbc57cc | |||
| 9bf0f624f5 | |||
| c6418789c4 | |||
| 8b909c54db | |||
| 9d029ac914 | |||
| 8644b3d163 | |||
| 426c5edf25 | |||
| 26cc7e2920 | |||
| 04eac3e7c5 | |||
| 7cf7575e6d | |||
| 111cea2381 | |||
| 896a3828f6 | |||
| eb02997836 | |||
| 8a33c171f5 | |||
| f87f279198 | |||
| 4cc019f038 | |||
| 34f4afad01 | |||
| 947b263b9c | |||
| 0a408e464a | |||
| 0d660277f6 | |||
| 33fef379ad | |||
| 0531369613 | |||
| c66f19108c | |||
| ef53367ba9 | |||
| 648fbf99fa |
@@ -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!
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -546,6 +546,18 @@ export const { cut } = registerControl('cut');
|
||||
*/
|
||||
export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp');
|
||||
|
||||
/**
|
||||
* Sets the lowpass filter lfo modulation depth.
|
||||
* @name lplfo
|
||||
* @param {number | Pattern} modulation depth of the lowpass filter lfo between 0 and _n_
|
||||
* @example
|
||||
* note("c2 e2 f2 g2")
|
||||
* .sound('sawtooth')
|
||||
* .lpf(300)
|
||||
* .lplfo(2)
|
||||
*/
|
||||
export const { lplfo } = registerControl('lplfo');
|
||||
|
||||
/**
|
||||
* Sets the lowpass filter envelope modulation depth.
|
||||
* @name lpenv
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
@@ -3100,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()),
|
||||
);
|
||||
|
||||
+39
-2
@@ -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);
|
||||
|
||||
@@ -5,9 +5,10 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
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 } 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,...
|
||||
@@ -498,7 +530,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), t
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -639,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 = {};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/serial
|
||||
|
||||
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.
|
||||
@@ -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 });
|
||||
});
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -112,11 +112,27 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
|
||||
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||
};
|
||||
|
||||
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
|
||||
export function createFilter(
|
||||
context,
|
||||
type,
|
||||
frequency,
|
||||
Q,
|
||||
att,
|
||||
dec,
|
||||
sus,
|
||||
rel,
|
||||
fenv,
|
||||
start,
|
||||
end,
|
||||
fanchor,
|
||||
model,
|
||||
drive,
|
||||
flfo,
|
||||
) {
|
||||
const curve = 'exponential';
|
||||
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
||||
let filter;
|
||||
let frequencyParam;
|
||||
let frequencyParam, detuneParam;
|
||||
if (model === 'ladder') {
|
||||
filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive });
|
||||
frequencyParam = filter.parameters.get('frequency');
|
||||
@@ -126,6 +142,7 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe
|
||||
filter.Q.value = Q;
|
||||
filter.frequency.value = frequency;
|
||||
frequencyParam = filter.frequency;
|
||||
detuneParam = filter.detune;
|
||||
}
|
||||
|
||||
// envelope is active when any of these values is set
|
||||
@@ -140,7 +157,10 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe
|
||||
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
|
||||
if (fenv < 0) [min, max] = [max, min];
|
||||
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve);
|
||||
return filter;
|
||||
}
|
||||
const hasLFO = detuneParam && flfo !== undefined;
|
||||
if (hasLFO !== undefined) {
|
||||
let lfo = getVibratoOscillator(detuneParam, { vib: flfo, vibmod: 4 }, start);
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -356,6 +357,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
lpdecay,
|
||||
lpsustain,
|
||||
lprelease,
|
||||
lplfo,
|
||||
resonance = getDefaultValue('resonance'),
|
||||
// high pass
|
||||
hpenv,
|
||||
@@ -474,6 +476,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
fanchor,
|
||||
ftype,
|
||||
drive,
|
||||
lplfo,
|
||||
);
|
||||
chain.push(lp());
|
||||
if (ftype === '24db') {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Generated
+22
@@ -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 ]",
|
||||
@@ -4422,6 +4496,27 @@ exports[`runs examples > example "lpf" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lplfo" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 1/4 → 1/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 1/2 → 3/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 3/4 → 1/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 1/1 → 5/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 5/4 → 3/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 3/2 → 7/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 7/4 → 2/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 2/1 → 9/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 9/4 → 5/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 5/2 → 11/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 11/4 → 3/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 3/1 → 13/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 13/4 → 7/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 7/2 → 15/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
"[ 15/4 → 4/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lpq" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh cutoff:2000 resonance:0 ]",
|
||||
@@ -7508,6 +7603,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 +8927,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?)
|
||||
|
||||
@@ -45,20 +46,22 @@ But you can also control cc messages separately like this:
|
||||
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
|
||||
/>
|
||||
|
||||
# OSC/SuperDirt API
|
||||
# OSC/SuperDirt/StrudelDirt
|
||||
|
||||
In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider.
|
||||
Strudel also supports using [SuperDirt](https://github.com/musikinformatik/SuperDirt/) as a backend, although it requires some developer tooling to run.
|
||||
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
|
||||
|
||||
There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is SuperDirt with some optimisations for working with Strudel. (A longer term aim is to merge these optimisations back into mainline SuperDirt)
|
||||
|
||||
## Prequisites
|
||||
|
||||
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!
|
||||
|
||||
@@ -86,3 +89,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([
|
||||
|
||||
Reference in New Issue
Block a user