Compare commits

...

40 Commits

Author SHA1 Message Date
Felix Roos e3c34f22fd wip websocket client 2025-09-07 22:41:25 +02:00
froos f2608e712b Merge pull request 'Signal flow documentation' (#1504) from glossing/strudel:glossing/signal-flow-documentation into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1504
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2025-09-07 21:48:18 +02:00
froos 34b41fc26f Merge pull request 'Allow flattening of dirs with sample server' (#1529) from glossing/strudel:glossing/sample-server-with-flat into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1529
2025-09-07 21:27:28 +02:00
Switch Angel AKA Jade Rose f6e171fed8 Merge pull request 'fix: prevent orbit clicking by defining max number of channels in an orbit' (#1552) from orbitfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1552
2025-09-07 21:05:23 +02:00
Jade (Rose) Rowland 2511dcc09e working 2025-09-07 15:02:13 -04:00
Felix Roos 2fbdc9fdd7 hotfix: formatting 2025-09-06 00:43:03 +02:00
Felix Roos 0a96fa5896 hotfix: comment out tauri stuff to fix osc 2025-09-06 00:42:36 +02:00
froos 06ebcbff8c Merge pull request 'Padding for tooltips, preserving linebreaks in descriptions, not closing autocompletes on click' (#1521) from glossing/strudel:glossing/tooltip-padding-and-paragraphs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1521
2025-09-02 08:42:41 +02:00
Switch Angel AKA Jade Rose ff2c59a2a6 Merge pull request 'Update "restore defaults" to not delete patterns' (#1519) from glossing/strudel:glossing/dont-delete-patterns-on-default into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1519
2025-09-01 17:50:08 +02:00
Aria 9b9176325b Update restore defaults to not delete patterns 2025-09-01 17:50:08 +02:00
Switch Angel AKA Jade Rose e84c39837f Merge pull request 'feat: add the ability to control the speed and start time of the reverb IR' (#1501) from irspeed into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1501
2025-08-31 19:10:02 +02:00
Jade (Rose) Rowland 6527c68cda Merge branch 'main' into irspeed 2025-08-31 12:51:28 -04:00
Aria 6a09f54b25 Add vibrato to list of continuous modulators 2025-08-31 01:03:09 -05:00
yaxu dc6b766ab7 Update README.md 2025-08-30 12:47:06 +02:00
yaxu 9eb3c9410d Add verify link to mastodon a/c 2025-08-30 12:46:00 +02:00
Aria 189e650a73 Add flat flag, automatic json saving; filter out non-audio at base dir 2025-08-29 12:18:28 -05:00
Aria a5886bb9d4 Use white-space instead; fix scrolling; don't close autocomplete tooltips on click 2025-08-26 14:14:00 -05:00
Aria 877bc95a58 Switch to more specific class and fix p->div 2025-08-26 12:54:39 -05:00
Aria 18b1739b89 Add a space after paragraphs in descriptions and add padding to hover tooltips 2025-08-26 12:53:38 -05:00
Switch Angel AKA Jade Rose b54139376e Merge pull request 'Bug Fix: Duck on Mobile / Safari' (#1503) from glossing/strudel:glossing/ducks-on-mobile into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1503
2025-08-25 02:31:21 +02:00
Aria d64a0ef0eb Lint requires no empty blocks: pass comment 2025-08-25 02:31:21 +02:00
Aria 659071a4ee Ensure zeroGain is cleaned up after use; move start/stop to after callback assigned for safety 2025-08-25 02:31:21 +02:00
Aria 64f7bc4442 Working version of duck on iOS safari 2025-08-25 02:31:21 +02:00
Alex McLean 8c97f2e2d0 Merge pull request 'fix benchmarks' (#1517) from fix-bench into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1517
2025-08-23 18:39:52 +02:00
alex b8c98736c5 fix benchmarks 2025-08-23 17:29:15 +01:00
Aria a0fc52b1ec Final tweaks 2025-08-21 14:42:52 -05:00
froos 0693a32736 Merge pull request 'add --json flag to @strudel/sampler to generate a strudel.json' (#1513) from strudeljson-generate into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1513
2025-08-21 20:57:31 +02:00
Felix Roos 42a903ecc0 add --yes flag to readme, making sure first run works 2025-08-21 20:36:42 +02:00
Felix Roos da283eb55a doc: generate strudel.json 2025-08-21 20:36:17 +02:00
Felix Roos b12707316a bump @strudel/sampler to 0.2.1 2025-08-21 20:30:32 +02:00
Felix Roos 66aa3ac1da add --json flag to strudel sampler 2025-08-21 20:30:02 +02:00
froos d81b1ab65c Merge pull request 'remove hs2js postinstall' (#1510) from remove-postinstall into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1510
2025-08-21 10:04:28 +02:00
Aria e490774294 Mention order of effects 2025-08-19 16:06:18 -05:00
Aria 34e8a57472 Typos grammar etc 2025-08-19 11:32:07 -05:00
Aria 59c8d70714 Code format 2025-08-19 11:27:56 -05:00
Aria ce7cff2c3b Clean up wording on orbit routing 2025-08-19 11:26:25 -05:00
Aria 32fe73aba2 Update effects documentation to include information on signal flow 2025-08-19 11:10:34 -05:00
Jade (Rose) Rowland cf55d4c8d1 rm var 2025-08-18 11:49:11 -04:00
Jade (Rose) Rowland e7e636886d add tests 2025-08-17 17:21:36 -04:00
Jade (Rose) Rowland e9aef9e4d4 working 2025-08-17 16:49:10 -04:00
21 changed files with 424 additions and 69 deletions
+2 -2
View File
@@ -3,8 +3,6 @@
Live coding patterns on the web
https://strudel.cc/
Development is moving to https://codeberg.org/uzu/strudel
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
@@ -47,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
+8 -6
View File
@@ -56,11 +56,13 @@ const buildExamples = (examples) =>
export const Autocomplete = ({ doc, label }) =>
h`
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
${doc.description ? `<p class="autocomplete-info-function-description">${doc.description}</p>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
<div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
</div>
</div>
`[0];
@@ -98,4 +100,4 @@ export const strudelAutocomplete = (context) => {
};
export const isAutoCompletionEnabled = (enabled) =>
enabled ? [autocompletion({ override: [strudelAutocomplete] })] : [];
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
+6 -6
View File
@@ -1,11 +1,11 @@
import { describe, bench } from 'vitest';
import { calculateTactus, sequence, stack } from '../index.mjs';
import { calculateSteps, sequence, stack } from '../index.mjs';
const pat64 = sequence(...Array(64).keys());
describe('steps', () => {
calculateTactus(true);
calculateSteps(true);
bench(
'+tactus',
() => {
@@ -14,7 +14,7 @@ describe('steps', () => {
{ time: 1000 },
);
calculateTactus(false);
calculateSteps(false);
bench(
'-tactus',
() => {
@@ -25,7 +25,7 @@ describe('steps', () => {
});
describe('stack', () => {
calculateTactus(true);
calculateSteps(true);
bench(
'+tactus',
() => {
@@ -34,7 +34,7 @@ describe('stack', () => {
{ time: 1000 },
);
calculateTactus(false);
calculateSteps(false);
bench(
'-tactus',
() => {
@@ -43,4 +43,4 @@ describe('stack', () => {
{ time: 1000 },
);
});
calculateTactus(true);
calculateSteps(true);
+23
View File
@@ -1516,6 +1516,29 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
*
*/
export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
/**
* Sets speed of the sample for the impulse response.
* @name irspeed
* @param {string | Pattern} speed
* @example
* samples('github:switchangel/pad')
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5)
*
*/
export const { irspeed } = registerControl('irspeed');
/**
* Sets the beginning of the IR response sample
* @name irbegin
* @param {string | Pattern} begin between 0 and 1
* @synonyms ir
* @example
* samples('github:switchangel/pad')
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6)
*
*/
export const { irbegin } = registerControl('irbegin');
/**
* Sets the room size of the reverb, see `room`.
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
+1 -1
View File
@@ -6,7 +6,7 @@ let debounce = 1000,
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
+1 -1
View File
@@ -3277,7 +3277,7 @@ export const slice = register(
* @memberof Pattern
* @returns Pattern
* @example
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
*/
Pattern.prototype.onTriggerTime = function (func) {
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
+4 -4
View File
@@ -1,10 +1,10 @@
import { describe, bench } from 'vitest';
import { calculateTactus } from '../../core/index.mjs';
import { calculateSteps } from '../../core/index.mjs';
import { mini } from '../index.mjs';
describe('mini', () => {
calculateTactus(true);
calculateSteps(true);
bench(
'+tactus',
() => {
@@ -13,7 +13,7 @@ describe('mini', () => {
{ time: 1000 },
);
calculateTactus(false);
calculateSteps(false);
bench(
'-tactus',
() => {
@@ -21,5 +21,5 @@ describe('mini', () => {
},
{ time: 1000 },
);
calculateTactus(true);
calculateSteps(true);
});
+3 -3
View File
@@ -1,8 +1,8 @@
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
import { isTauri } from '../desktopbridge/utils.mjs';
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
import { isTauri } from '../desktopbridge/utils.mjs'; */
import { oscTrigger } from './osc.mjs';
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
const currentTime = performance.now() / 1000;
+10
View File
@@ -20,3 +20,13 @@ samples('http://localhost:5432')
LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port
```
## static json
when running with `--json`, you will simply get the json logged back:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
this is useful if you want to create a sample pack from the current folder.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.0",
"version": "0.2.2",
"description": "",
"keywords": [
"tidalcycles",
+69 -23
View File
@@ -1,22 +1,20 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream, existsSync } from 'fs';
import { createReadStream, existsSync, writeFileSync } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join, sep } from 'path';
import { join, resolve, sep } from 'path';
import readline from 'readline';
import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
const isAudioFile = (f) => {
const ext = f.split('.').slice(-1)[0].toLowerCase();
return VALID_AUDIO_EXTENSIONS.includes(ext);
};
async function getFilesInDirectory(directory) {
let files = [];
@@ -29,42 +27,90 @@ async function getFilesInDirectory(directory) {
continue;
}
try {
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
);
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
files = files.concat(subFiles);
LOG && console.log(`${dirent.name} (${subFiles.length})`);
} catch (err) {
LOG && console.warn(`skipped due to error: ${fullPath}`);
}
} else {
files.push(fullPath);
isAudioFile(fullPath) && files.push(fullPath);
}
}
return files;
}
async function getBanks(directory) {
async function getBanks(directory, flat = false) {
let files = await getFilesInDirectory(directory);
let banks = {};
directory = directory.split(sep).join('/');
files = files.map((path) => {
path = path.split(sep).join('/');
const [bank] = path.split('/').slice(-2);
const subDir = path.replace(directory, '');
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
banks[bank] = banks[bank] || [];
const relativeUrl = path.replace(directory, '');
banks[bank].push(relativeUrl);
return relativeUrl;
banks[bank].push(subDir);
return subDir;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
// eslint-disable-next-line
const directory = process.cwd();
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
function getInput(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(query, (response) => {
rl.close();
resolve(response);
}),
);
}
let directory = getArgValue('--dir') || process.cwd();
directory = resolve(directory);
if (args.includes('--json')) {
const { banks } = await getBanks(directory, getArgValue('--flat'));
const json = JSON.stringify(banks);
const outFile = resolve(directory, 'strudel.json');
if (existsSync(outFile)) {
const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
if (answer.toLowerCase() !== 'y') {
console.log('Aborted.');
process.exit(0);
}
}
writeFileSync(outFile, json, 'utf8');
console.log(`Wrote json to ${outFile}`);
}
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory);
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
if (req.url === '/') {
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify(banks));
@@ -72,7 +118,7 @@ const server = http.createServer(async (req, res) => {
let subpath = decodeURIComponent(req.url);
const filePath = join(directory, subpath.split('/').join(sep));
//console.log('GET:', filePath);
// console.log('GET:', filePath);
const isFound = existsSync(filePath);
if (!isFound) {
res.statusCode = 404;
+20 -2
View File
@@ -211,11 +211,29 @@ export function getVibratoOscillator(param, value, t) {
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = new ConstantSourceNode(audioContext);
constantNode.start(startTime);
constantNode.stop(stopTime);
// Certain browsers requires audio nodes to be connected in order for their onended events
// to fire, so we _mute it_ and then connect it to the destination
const zeroGain = gainNode(0);
zeroGain.connect(audioContext.destination);
constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => {
// Ensure garbage collection
try {
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete();
};
constantNode.start(startTime);
constantNode.stop(stopTime);
return constantNode;
}
const mod = (freq, range = 1, type = 'sine') => {
+16 -6
View File
@@ -1,7 +1,9 @@
import reverbGen from './reverbGen.mjs';
import { clamp } from './util.mjs';
if (typeof AudioContext !== 'undefined') {
AudioContext.prototype.adjustLength = function (duration, buffer) {
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
const newLength = buffer.sampleRate * duration;
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
@@ -9,22 +11,30 @@ if (typeof AudioContext !== 'undefined') {
let newData = newBuffer.getChannelData(channel);
for (let i = 0; i < newLength; i++) {
newData[i] = oldData[i] || 0;
// loop the buffer around to prevent
let position = (sampleOffset + i * Math.abs(speed)) % oldData.length;
if (speed < 1) {
position = position * -1;
}
newData[i] = oldData.at(position) || 0;
}
}
return newBuffer;
};
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
const convolver = this.createConvolver();
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
convolver.duration = d;
convolver.fade = fade;
convolver.lp = lp;
convolver.dim = dim;
convolver.ir = ir;
convolver.irspeed = irspeed;
convolver.irbegin = irbegin;
if (ir) {
convolver.buffer = this.adjustLength(d, ir);
convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin);
} else {
reverbGen.generateReverb(
{
@@ -41,7 +51,7 @@ if (typeof AudioContext !== 'undefined') {
);
}
};
convolver.generate(duration, fade, lp, dim, ir);
convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
return convolver;
};
}
+12 -8
View File
@@ -282,7 +282,6 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
let delays = {};
const maxfeedback = 0.98;
let channelMerger, destinationGain;
@@ -326,7 +325,7 @@ export const panic = () => {
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
@@ -414,7 +413,7 @@ function connectToOrbit(node, orbit) {
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1 }),
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].gain, channels);
}
@@ -442,19 +441,22 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir) {
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (!orbits[orbit].reverbNode) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
connectToOrbit(reverb, orbit);
orbits[orbit].reverbNode = reverb;
}
if (
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
orbits[orbit].reverbNode.ir !== ir
) {
// only regenerate when something has changed
@@ -462,7 +464,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return orbits[orbit].reverbNode;
}
@@ -619,6 +621,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomdim,
roomsize,
ir,
irspeed,
irbegin,
i = getDefaultValue('i'),
velocity = getDefaultValue('velocity'),
analyze, // analyser wet
@@ -832,7 +836,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
@@ -850,7 +854,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}
+74
View File
@@ -4875,6 +4875,43 @@ exports[`runs examples > example "irand" example index 0 1`] = `
]
`;
exports[`runs examples > example "irbegin" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
]
`;
exports[`runs examples > example "iresponse" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]",
@@ -4896,6 +4933,43 @@ exports[`runs examples > example "iresponse" example index 0 1`] = `
]
`;
exports[`runs examples > example "irspeed" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
]
`;
exports[`runs examples > example "isaw" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:c3 clip:1 ]",
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+123
View File
@@ -11,6 +11,129 @@ import { JsDoc } from '../../docs/JsDoc';
Whether you're using a synth or a sample, you can apply any of the following built-in audio effects.
As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.
# Signal chain
<img src="/img/strudel-signal-flow.png"></img>
The signal chain in Strudel is as follows:
- An sound-generating event is triggered by a pattern
- This has a start time and a duration, which is usually
controlled by the note length and ADSR parameters
- If we exceed the max polyphony, old sounds begin to die off
- Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped
- A sound is produced (through, say, a sample or an oscillator)
- This is where detune-based effects (like `detune`, `penv`, etc. occur)
- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are
single use effects, meaning that multiple occurrences of them in a pattern will simply override the values
(e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass
again)
- Phase vocoder (`stretch`)
- Gain is applied (`gain`)
- This is where the main (volume) ADSR happens
- A lowpass filter (`lpf`)
- A highpass filter (`hpf`)
- A bandpass filter (`bandpass`)
- A vowel filter (`vowel`)
- Sample rate reduction (`coarse`)
- Bit crushing (`crush`)
- Waveshape distortion (`shape`)
- Normal distortion (`distort`)
- Tremolo (`tremolo`)
- Compressor (`compressor`)
- Panning (`pan`)
- Phaser (`phaser`)
- Postgain (`post`)
- The sound is then split into multiple destinations
- Dry output (amount controlled by `dry` parameter)
- The sends
- Analyzers
- These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes
- Delay (amount controlled by `delay` parameter)
- Reverb (amount controlled by `room` parameter)
- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below)
- The `duck` effect affects the volume of all signals in the orbit
- The orbit is then sent to the mixer
## Orbits
Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal.
By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting
(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to
to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing.
The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation
<MiniRepl client:visible tune={`s("white").orbit("2,3,4").gain(0.2)`} />
but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed
down to a single output, they will triple the volume. We've reduced the gain here to save your ears.
⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two
patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck
with a large reverb:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10)`}
/>
versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10)
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
/>
This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be
distinct:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10).orbit(2)
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
/>
## Continuous changes
As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be
modified continuously in time. For example,
<MiniRepl
client:visible
tune={`
s("supersaw").lpf(tri.range(100, 5000).slow(2))`}
/>
Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits
(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.:
<MiniRepl
client:visible
tune={`
s("supersaw").seg(16).lpf(tri.range(100, 5000).slow(2))`}
/>
Some parameters _do_ induce continuous variations in time, though:
- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`)
- The pitch envelope curve (governed by `penv` and its associated ADSR)
- The FM curve (`fmenv`)
- The filter envelopes (`lpenv`, `hpenv`, `bpenv`)
- Tremolo (`tremolo`)
- Phaser (`phaser`)
- Vibrato (`vib`)
- Ducking (`duckorbit`)
# Filters
Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis).
+10
View File
@@ -178,6 +178,16 @@ the version number).
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
or from the dev console if you're technically minded, or by using a cache deleting extension).
## Generating strudel.json
You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler".
## Github Shortcut
Because loading samples from github is common, there is a shortcut:
+13 -5
View File
@@ -72,21 +72,19 @@
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
.cm-tooltip.cm-completionInfo {
padding: 12px !important;
padding-bottom: 12px !important;
padding: 0 !important;
border: 1px solid var(--foreground) !important;
border-radius: 4px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
max-width: 500px !important;
min-width: 300px !important;
max-height: 400px !important;
white-space: normal !important;
overflow: auto !important;
background-color: var(--lineHighlight) !important;
}
/* Main tooltip container */
.autocomplete-info-tooltip {
.autocomplete-info-container {
padding: 12px !important;
border-radius: 4px !important;
color: var(--foreground);
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
@@ -95,6 +93,16 @@
max-width: 600px;
max-height: 400px;
min-width: 400px;
white-space: normal !important;
overflow-y: auto !important;
}
.autocomplete-info-tooltip {
overflow-y: auto !important;
}
.autocomplete-info-function-description {
white-space: pre-wrap !important;
}
.autocomplete-info-function-name {
@@ -311,7 +311,8 @@ export function SettingsTab({ started }) {
onClick={() => {
confirmDialog('Sure?').then((r) => {
if (r) {
settingsMap.set(defaultSettings);
const { userPatterns } = settingsMap.get(); // keep current patterns
settingsMap.set({ ...defaultSettings, userPatterns });
}
});
}}
+26
View File
@@ -0,0 +1,26 @@
import { register, signal } from '@strudel/core';
import { clamp } from '../../../packages/superdough/util.mjs';
let sigs = [0, 0, 0, 0, 0, 0, 0, 0];
export let sig = register('sig', (pat) =>
pat
.fmap((n) => {
return signal(() => clamp(sigs[n], 0.0001, 1));
})
.outerJoin(),
);
let socket;
export function initSocket(ip) {
// e.g. 192.168.1.12:4422
socket = socket || new WebSocket(`ws://${ip}`);
socket.addEventListener('open', () => console.log('connection opened'));
socket.addEventListener('error', () => console.log('connection error'));
socket.addEventListener('message', (event) => {
if (Math.random() < 0.01) {
console.log(event.data.toString());
}
sigs = JSON.parse(event.data.toString());
});
}