Merge branch 'main' into main

This commit is contained in:
froos 2025-10-20 21:21:08 +02:00
commit 57f2a97b7b
6 changed files with 50 additions and 21 deletions

View file

@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples')
The format is `github:<user>/<repo>/<branch>`. The format is `github:<user>/<repo>/<branch>`.
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo. It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
The format is also expected to be the same as explained above. The format is also expected to be the same as explained above.

View file

@ -121,13 +121,20 @@ function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) { if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl'); throw new Error('expected "github:" at the start of pseudoUrl');
} }
let [_, path] = base.split('github:'); let path = base.slice('github:'.length);
path = path.endsWith('/') ? path.slice(0, -1) : path; path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set let components = path.split('/');
path += '/main'; let user = components[0];
let repo = components.length >= 2 ? components[1] : 'samples';
let branch = components.length >= 3 ? components[2] : 'main';
let other = components.slice(3);
if (subpath) {
other.push(subpath);
} }
return `https://raw.githubusercontent.com/${path}/${subpath}`; other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
} }
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {

View file

@ -231,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
begin: t, begin: t,
end: envEnd, end: envEnd,
frequency, frequency,
detune: value.detune, freqspread: value.detune,
position: value.wt, position: value.wt,
warp: value.warp, warp: value.warp,
warpMode: warpmode, warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1), voices: Math.max(value.unison ?? 1, 1),
spread: value.spread, panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
}, },
{ outputChannelCount: [2] }, { outputChannelCount: [2] },

View file

@ -1050,14 +1050,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return [ return [
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, { name: 'frequency', defaultValue: 440, min: Number.EPSILON },
{ name: 'detune', defaultValue: 0.18 }, { name: 'detune', defaultValue: 0 },
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'freqspread', defaultValue: 0.18, min: 0 },
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'position', defaultValue: 0, min: 0, max: 1 },
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
{ name: 'warpMode', defaultValue: 0 }, { name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, { name: 'voices', defaultValue: 1, min: 1 },
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 }, { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
]; ];
} }
@ -1235,10 +1236,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_sampleFrame(frame, phase) { _sampleFrame(frame, phase) {
const len = frame.length; const len = frame.length;
const pos = phase * len; const pos = phase * len;
const i = pos | 0; let i = pos | 0;
if (i >= len) i = 0; // fast wrap
const frac = pos - i; const frac = pos - i;
const a = frame[i]; const a = frame[i];
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap let i1 = i + 1;
if (i1 >= len) i1 = 0;
const b = frame[i1]; const b = frame[i1];
return a + (b - a) * frac; return a + (b - a) * frac;
} }
@ -1268,6 +1271,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
for (let i = 0; i < outL.length; i++) { for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i); const detune = pv(parameters.detune, i);
const freqspread = pv(parameters.freqspread, i);
const tablePos = clamp(pv(parameters.position, i), 0, 1); const tablePos = clamp(pv(parameters.position, i), 0, 1);
const idx = tablePos * (this.numFrames - 1); const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0; const fIdx = idx | 0;
@ -1276,9 +1280,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const warpMode = pv(parameters.warpMode, i); const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i); const voices = pv(parameters.voices, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0; const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * spread); const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
const gain2 = Math.sqrt(0.5 + 0.5 * spread); const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
let f = pv(parameters.frequency, i); let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices); const normalizer = 1 / Math.sqrt(voices);
@ -1291,7 +1295,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2; gainL = gain2;
gainR = gain1; gainR = gain1;
} }
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
const dPhase = fVoice * this.invSR; const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase); const level = this._chooseMip(dPhase);
const table = this.tables[level]; const table = this.tables[level];

View file

@ -60,6 +60,23 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
.room(0.5)`} .room(0.5)`}
/> />
## Write your own chained function
You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function.
<MiniRepl
client:idle
tune={`const effectChain = register('effectChain', (pat) => pat
.s("sawtooth")
.cutoff(500)
//.delay(0.5)
.room(0.5)
)
note("a3 c#4 e4 a4").effectChain()`}
/>
Try adding `.rev()` after `effectChain()` to hear further effects added.
# Comments # Comments
The `//` in the example above is a line comment, resulting in the `delay` function being ignored. The `//` in the example above is a line comment, resulting in the `delay` function being ignored.

View file

@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
### scrub ### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '} <JsDoc client:idle name="Pattern.scrub" h={0} />
### speed ### speed