Compare commits

..

3 Commits

Author SHA1 Message Date
Felix Roos 4007e952ac move prettier plugin to separate file + add alt shift f 2024-12-16 22:36:49 +01:00
jack-arms 1873377f60 pnpm run codeformat 2024-12-03 16:19:13 -08:00
Jack Armstrong 3747c5b390 add formatting shortcut to use prettier 2024-11-23 18:48:20 -08:00
32 changed files with 107 additions and 783 deletions
-12
View File
@@ -138,18 +138,6 @@ 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!
+2
View File
@@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { prettierPlugin } from './prettier.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -77,6 +78,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
javascript(),
sliderPlugin,
widgetPlugin,
prettierPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
syntaxHighlighting(defaultHighlightStyle),
+2 -1
View File
@@ -49,7 +49,8 @@
"@strudel/transpiler": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5"
"nanostores": "^0.9.5",
"prettier": "^3.3.3"
},
"devDependencies": {
"vite": "^5.0.10"
+55
View File
@@ -0,0 +1,55 @@
import { EditorSelection } from '@codemirror/state';
import * as prettier from 'prettier/standalone';
import typescriptPlugin from 'prettier/plugins/typescript';
import estreePlugin from 'prettier/plugins/estree';
import { keymap } from '@codemirror/view';
export async function runPrettier(editorView) {
const currentState = editorView.state.doc.toString();
// Prettier insists on consistent quotes, but in Strudel double quotes are interpreted
// as patterns and single quotes are for everything else, so a consistent setting won't work.
// It's a great formatter though, so as a workaround it works to put "// prettier-ignore" comments
// before the single quoted stuff to preserve it, but this is a pain to make users to, so an extra
// hack is to take the preformatted code, insert these comments behind the scenes, format, then
// remove them before setting the editor state.
const preFormat = currentState
.split('\n')
.map((line) => (line.match(/'.*'/) != null ? '// prettier-ignore\n' + line : line))
.join('\n');
console.log(preFormat);
const formattedState = (
await prettier.format(preFormat, {
parser: 'typescript',
plugins: [typescriptPlugin, estreePlugin],
semi: false,
})
).replace(/.*\/\/ prettier-ignore.*\n/g, '');
editorView.dispatch({
changes: { from: 0, to: editorView.state.doc.length, insert: formattedState },
selection: EditorSelection.single(
// keep cursor close to the original position, but also keep it within the bounds
// of the formatted document
Math.min(editorView.state.selection.main.to, formattedState.length),
),
scrollIntoView: true,
});
}
export const prettierPlugin = keymap.of([
{
key: 'Alt-,',
preventDefault: true,
run: runPrettier,
},
{
key: 'Ctrl-,',
preventDefault: true,
run: runPrettier,
},
{
key: 'Alt-Shift-f',
preventDefault: true,
run: runPrettier,
},
]);
-7
View File
@@ -133,10 +133,3 @@ 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 });
});
-12
View File
@@ -546,18 +546,6 @@ 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
-4
View File
@@ -68,10 +68,6 @@ 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) {
-35
View File
@@ -1023,7 +1023,6 @@ 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)],
@@ -1492,14 +1491,6 @@ 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));
@@ -2548,10 +2539,6 @@ 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(
@@ -3113,25 +3100,3 @@ 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()),
);
+2 -39
View File
@@ -63,13 +63,11 @@ export function repl({
let pPatterns = {};
let anonymousIndex = 0;
let allTransform;
let eachTransform;
const hush = function () {
pPatterns = {};
anonymousIndex = 0;
allTransform = undefined;
eachTransform = undefined;
return silence;
};
@@ -86,42 +84,15 @@ 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 (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) {
if (id.startsWith('_') || id.endsWith('_')) {
// allows muting a pattern x with x_ or _x
return silence;
}
@@ -160,7 +131,6 @@ export function repl({
});
return evalScope({
all,
each,
hush,
cpm,
setCps,
@@ -182,14 +152,7 @@ export function repl({
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) {
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);
pattern = stack(...Object.values(pPatterns));
}
if (allTransform) {
pattern = allTransform(pattern);
+3 -80
View File
@@ -5,10 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Hap } from './hap.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
import { id, _mod } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -160,37 +159,6 @@ 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,...
@@ -530,7 +498,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();
});
@@ -671,48 +639,3 @@ 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);
});
-23
View File
@@ -46,14 +46,12 @@ import {
rev,
time,
run,
binaryN,
pick,
stackLeft,
stackRight,
stackCentre,
s_cat,
calculateTactus,
sometimes,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -960,18 +958,6 @@ 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(
@@ -1267,13 +1253,4 @@ 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);
});
});
});
-32
View File
@@ -434,38 +434,6 @@ 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) {
+1 -1
View File
@@ -26,7 +26,7 @@ export const getDrawContext = (id = 'test-canvas', options) => {
}, 200);
});
}
return canvas.getContext(contextType, { willReadFrequently: true });
return canvas.getContext(contextType);
};
let animationFrames = {};
-3
View File
@@ -1,3 +0,0 @@
# @strudel/serial
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.
-87
View File
@@ -1,87 +0,0 @@
/*
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
View File
@@ -1,38 +0,0 @@
{
"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
View File
@@ -1,19 +0,0 @@
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',
},
});
+3 -23
View File
@@ -112,27 +112,11 @@ 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,
flfo,
) {
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
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, detuneParam;
let frequencyParam;
if (model === 'ladder') {
filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive });
frequencyParam = filter.parameters.get('frequency');
@@ -142,7 +126,6 @@ export function createFilter(
filter.Q.value = Q;
filter.frequency.value = frequency;
frequencyParam = filter.frequency;
detuneParam = filter.detune;
}
// envelope is active when any of these values is set
@@ -157,10 +140,7 @@ export function createFilter(
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);
}
const hasLFO = detuneParam && flfo !== undefined;
if (hasLFO !== undefined) {
let lfo = getVibratoOscillator(detuneParam, { vib: flfo, vibmod: 4 }, start);
return filter;
}
return filter;
}
+1 -4
View File
@@ -270,12 +270,11 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
export let analysers = {},
analysersData = {};
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
export function getAnalyserById(id, fftSize = 1024) {
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);
@@ -357,7 +356,6 @@ export const superdough = async (value, t, hapDuration) => {
lpdecay,
lpsustain,
lprelease,
lplfo,
resonance = getDefaultValue('resonance'),
// high pass
hpenv,
@@ -476,7 +474,6 @@ export const superdough = async (value, t, hapDuration) => {
fanchor,
ftype,
drive,
lplfo,
);
chain.push(lp());
if (ftype === '24db') {
-1
View File
@@ -6,5 +6,4 @@ 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
View File
@@ -1,69 +0,0 @@
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));
}
+3 -21
View File
@@ -212,6 +212,9 @@ importers:
nanostores:
specifier: ^0.9.5
version: 0.9.5
prettier:
specifier: ^3.3.3
version: 3.3.3
devDependencies:
vite:
specifier: ^5.0.10
@@ -331,19 +334,6 @@ 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':
@@ -636,9 +626,6 @@ 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
@@ -6007,9 +5994,6 @@ 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'}
@@ -14706,8 +14690,6 @@ snapshots:
- bluebird
- supports-color
paho-mqtt@1.1.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
-166
View File
@@ -954,24 +954,6 @@ 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 ]",
@@ -985,60 +967,6 @@ 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 ]",
@@ -4137,8 +4065,6 @@ 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 ]",
@@ -4496,27 +4422,6 @@ 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 ]",
@@ -7603,75 +7508,6 @@ 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 ]",
@@ -8927,8 +8763,6 @@ 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 ]",
+6 -4
View File
@@ -8,7 +8,6 @@ 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 ]",
@@ -18,20 +17,22 @@ 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 ]",
@@ -6905,10 +6906,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 ]",
@@ -6916,6 +6917,7 @@ 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 ]",
]
`;
-3
View File
@@ -134,9 +134,6 @@ 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;
};
-1
View File
@@ -33,7 +33,6 @@
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/mqtt": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/serial": "workspace:*",
"@strudel/soundfonts": "workspace:*",
@@ -82,7 +82,18 @@ 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()`} />
Remember the execution of the chained functions goes from left to right.
- 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.
# Operators
+1 -9
View File
@@ -55,14 +55,6 @@ These are the equivalents used by the Mini Notation:
## run
<JsDoc client:idle name="run" h={0} />
## binary
<JsDoc client:idle name="binary" h={0} />
## binaryN
<JsDoc client:idle name="binaryN" h={0} />
<JsDoc client:idle name="run" h={0} punchcard />
After Pattern Constructors, let's see what [Time Modifiers](/learn/time-modifiers) are available.
+14 -81
View File
@@ -1,20 +1,19 @@
---
title: MIDI, OSC & MQTT
title: MIDI & OSC
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# MIDI, OSC and MQTT
# MIDI and OSC
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).
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.
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.
# MIDI API
# MIDI
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
## midi(outputName?)
@@ -46,22 +45,20 @@ But you can also control cc messages separately like this:
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
/>
# OSC/SuperDirt/StrudelDirt
# OSC/SuperDirt API
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)
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.
## Prequisites
To get SuperDirt to work with Strudel, you need to
Getting [SuperDirt](https://github.com/musikinformatik/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 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
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
Now you're all set!
@@ -89,67 +86,3 @@ 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,10 +99,6 @@ 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 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!
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!
</p>
{visibleFunctions.map((entry, i) => (
<section key={i}>
-1
View File
@@ -81,7 +81,6 @@ export function loadModules() {
import('@strudel/soundfonts'),
import('@strudel/csound'),
import('@strudel/tidal'),
import('@strudel/mqtt'),
];
if (isTauri()) {
modules = modules.concat([