mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34ae1bb305 | |||
| 0cf11adc2c | |||
| f7f8c56c0a | |||
| c7e88e1ff2 | |||
| f6a43f1b10 | |||
| a3b183d304 | |||
| f7175c0505 | |||
| e26040d154 | |||
| 17a1b75e5b | |||
| 7df9688bb9 | |||
| d557bb59f0 | |||
| 679e81eb0f | |||
| 37dd834398 | |||
| 959e8ac7bc | |||
| 16524ca805 | |||
| b4b0194455 | |||
| fd472a64d9 | |||
| 58094973b0 | |||
| db49b37780 | |||
| bf243c04b9 | |||
| bc05cffdbc | |||
| 7b1e02e8cf | |||
| 257f09da4f | |||
| ecd58fa501 | |||
| c4ece4932e | |||
| 5c9855bf12 | |||
| e54b9c3a32 | |||
| 6afe0dc714 | |||
| d41ebf190b | |||
| 1823993e7c | |||
| d349d2a0cd | |||
| a79491dd16 | |||
| 8529516c66 |
@@ -1,14 +1,17 @@
|
||||
# strudel
|
||||
|
||||
Live coding patterns on the web
|
||||
https://strudel.cc/
|
||||
|
||||
|
||||
- Try it here: <https://strudel.cc>
|
||||
- Docs: <https://strudel.cc/learn>
|
||||
- Source: https://codeberg.org/uzu/strudel/
|
||||
* Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. **Please don't fork the project back to github**.
|
||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
|
||||
|
||||
|
||||
## Running Locally
|
||||
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
+17
-14
@@ -2844,7 +2844,7 @@ registerSubControls('lfo', [
|
||||
['dcoffset', 'dc'],
|
||||
['shape', 'sh'],
|
||||
['skew', 'sk'],
|
||||
['curve'],
|
||||
['curve', 'cu'],
|
||||
['sync', 's'],
|
||||
['fxi'],
|
||||
]);
|
||||
@@ -2872,7 +2872,7 @@ registerSubControls('bmod', [
|
||||
['fxi'],
|
||||
]);
|
||||
|
||||
Pattern.prototype.modulate = function (type, config, id) {
|
||||
Pattern.prototype.modulate = function (type, config, idPat) {
|
||||
config = { control: undefined, ...config };
|
||||
const modulatorKeys = ['lfo', 'env', 'bmod'];
|
||||
if (!modulatorKeys.includes(type)) {
|
||||
@@ -2881,11 +2881,14 @@ Pattern.prototype.modulate = function (type, config, id) {
|
||||
}
|
||||
let output = this;
|
||||
let defaultValue = undefined;
|
||||
// Copy value into a temporary `v` container and attach a single `id` (to be shared across
|
||||
// each config entry). At the output we destructure and throw away the id
|
||||
output = output.fmap((v) => (id) => ({ v, id })).appLeft(reify(idPat));
|
||||
for (const [rawKey, value] of Object.entries(config)) {
|
||||
const key = getMainSubcontrolName(type, rawKey);
|
||||
const valuePat = reify(value);
|
||||
output = output
|
||||
.fmap((v) => (c) => {
|
||||
.fmap(({ v, id }) => (c) => {
|
||||
if (defaultValue === undefined) {
|
||||
// default control to the control set just before this in the chain
|
||||
// e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO
|
||||
@@ -2900,17 +2903,17 @@ Pattern.prototype.modulate = function (type, config, id) {
|
||||
id ??= t.__ids.size;
|
||||
t[id] ??= { control: defaultValue };
|
||||
t.__ids.add(id); // keeps track of insertion order
|
||||
if (c === undefined) return v;
|
||||
if (c === undefined) return { v, id };
|
||||
if (key === 'control' || key === 'subControl') {
|
||||
t[id][key] = getControlName(c);
|
||||
} else {
|
||||
t[id][key] = c;
|
||||
}
|
||||
return v;
|
||||
return { v, id };
|
||||
})
|
||||
.appLeft(valuePat);
|
||||
}
|
||||
return output;
|
||||
return output.fmap(({ v }) => v);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2925,15 +2928,15 @@ Pattern.prototype.modulate = function (type, config, id) {
|
||||
*
|
||||
* @name lfo
|
||||
* @param {Object} config LFO configuration.
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p
|
||||
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: r
|
||||
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||
* @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc
|
||||
* @param {number | Pattern} [config.shape] Shape index. Aliases: sh
|
||||
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
|
||||
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c
|
||||
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu
|
||||
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
|
||||
* @param {number | Pattern} [config.fxi] FX index to target
|
||||
* @param {string | Pattern} id ID to use for this modulator
|
||||
@@ -2978,8 +2981,8 @@ export const lfo = (config) => pure({}).lfo(config);
|
||||
*
|
||||
* @name env
|
||||
* @param {Object} config Envelope configuration.
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||
* @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a
|
||||
@@ -3039,8 +3042,8 @@ export const env = (config) => pure({}).env(config);
|
||||
* @name bmod
|
||||
* @param {Object} config Bus modulation configuration.
|
||||
* @param {string | Pattern} [config.bus] Bus to get modulation signal from
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||
* @param {number | Pattern} [config.dc] DC offset prior to application
|
||||
|
||||
+31
-10
@@ -3586,6 +3586,20 @@ export const morph = (frompat, topat, bypat) => {
|
||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||
};
|
||||
|
||||
const _distortWithAlg = function (name) {
|
||||
const func = function (args, pat) {
|
||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||
if (!pat) {
|
||||
return pure({}).distort(argsPat);
|
||||
}
|
||||
return pat.distort(argsPat);
|
||||
};
|
||||
Pattern.prototype[name] = function (args) {
|
||||
return func(args, this);
|
||||
};
|
||||
return func;
|
||||
};
|
||||
|
||||
/**
|
||||
* Soft-clipping distortion
|
||||
*
|
||||
@@ -3594,6 +3608,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const soft = _distortWithAlg('soft');
|
||||
|
||||
/**
|
||||
* Hard-clipping distortion
|
||||
*
|
||||
@@ -3602,6 +3618,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const hard = _distortWithAlg('hard');
|
||||
|
||||
/**
|
||||
* Cubic polynomial distortion
|
||||
*
|
||||
@@ -3610,6 +3628,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const cubic = _distortWithAlg('cubic');
|
||||
|
||||
/**
|
||||
* Diode-emulating distortion
|
||||
*
|
||||
@@ -3618,6 +3638,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const diode = _distortWithAlg('diode');
|
||||
|
||||
/**
|
||||
* Asymmetrical diode distortion
|
||||
*
|
||||
@@ -3626,6 +3648,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const asym = _distortWithAlg('asym');
|
||||
|
||||
/**
|
||||
* Wavefolding distortion
|
||||
*
|
||||
@@ -3634,6 +3658,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const fold = _distortWithAlg('fold');
|
||||
|
||||
/**
|
||||
* Wavefolding distortion composed with sinusoid
|
||||
*
|
||||
@@ -3642,6 +3668,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const sinefold = _distortWithAlg('sinefold');
|
||||
|
||||
/**
|
||||
* Distortion via Chebyshev polynomials
|
||||
*
|
||||
@@ -3650,14 +3678,7 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
||||
for (const name of distAlgoNames) {
|
||||
// Add aliases for distortion algorithms
|
||||
Pattern.prototype[name] = function (args) {
|
||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||
return this.distort(argsPat);
|
||||
};
|
||||
}
|
||||
export const chebyshev = _distortWithAlg('chebyshev');
|
||||
|
||||
/**
|
||||
* Turns a list of patterns into a single pattern which outputs list-values
|
||||
@@ -3723,8 +3744,8 @@ export const phases = (list) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Establishes an FX chain. Can be called by chaining .FX(<fx1>).FX(<fx2>)..
|
||||
* calls and/or in a single .FX(<fx1>, <fx2>, ..) call. The <fx1>, .. are _patterns_ which
|
||||
* Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2)..
|
||||
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
|
||||
* establish the controls of the given effect. See examples.
|
||||
* @name FX
|
||||
* @memberof Pattern
|
||||
|
||||
@@ -46,4 +46,4 @@ all(osc)
|
||||
|
||||
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
|
||||
|
||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
||||
You can read more about [how to use Superdirt with Strudel](https://strudel.cc/learn/input-output/#oscsuperdirtstrudeldirt) in the tutorial.
|
||||
|
||||
@@ -461,10 +461,11 @@ export function applyFM(param, value, begin) {
|
||||
nodes[`fm_${idx}`] = [osc];
|
||||
}
|
||||
const { input, output, freq, osc, toCleanup } = fms[idx];
|
||||
const g = gainNode(amt * freq);
|
||||
io.push(isMod ? output.connect(g) : input);
|
||||
cleanupOnEnd(osc, [...toCleanup, g]);
|
||||
nodes[`fm_${idx}_gain`] = [g];
|
||||
const gAmt = gainNode(amt);
|
||||
const gFreq = gainNode(freq);
|
||||
io.push(isMod ? output.connect(gAmt).connect(gFreq) : input);
|
||||
cleanupOnEnd(osc, [...toCleanup, gAmt, gFreq]);
|
||||
nodes[`fm_${idx}_gain`] = [gAmt];
|
||||
}
|
||||
if (!io[1]) {
|
||||
logger(
|
||||
|
||||
@@ -32,8 +32,9 @@ const getNodeParam = (node, name) => {
|
||||
|
||||
const controlTargets = getSuperdoughControlTargets();
|
||||
|
||||
const getControlData = (control) => {
|
||||
return controlTargets[control.split('_')[0]];
|
||||
const getControlData = (control, subControl) => {
|
||||
const controlNoIdx = control.split('_')[0];
|
||||
return controlTargets[`${controlNoIdx}_${subControl}`] ?? controlTargets[controlNoIdx];
|
||||
};
|
||||
|
||||
const getRangeForParam = (paramName, currentValue) => {
|
||||
@@ -59,8 +60,7 @@ const clampWithWaveShaper = (modulator, min, max) => {
|
||||
};
|
||||
|
||||
const getTargetParamsForControl = (control, nodes, subControl) => {
|
||||
const lookupKey = subControl ? `${control}_${subControl}` : control;
|
||||
const targetInfo = getControlData(lookupKey) ?? getControlData(control);
|
||||
const targetInfo = getControlData(control, subControl);
|
||||
if (!targetInfo) {
|
||||
errorLogger(
|
||||
new Error(`Could not find control data for target '${control}'. It may not be modulatable.`),
|
||||
|
||||
@@ -65,21 +65,22 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
|
||||
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
const bufferDuration = bufferSource.buffer.duration;
|
||||
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
// The computation of the offset into the sound is performed using the sound buffer's natural duration,
|
||||
// rather than the playback duration, so that even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5.
|
||||
const offset = begin * bufferDuration;
|
||||
|
||||
const loop = hapValue.loop;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopStart = loopBegin * bufferDuration;
|
||||
bufferSource.loopEnd = loopEnd * bufferDuration;
|
||||
}
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
const sliceDuration = (end - begin) * bufferDuration;
|
||||
return { bufferSource, offset, bufferDuration, sliceDuration };
|
||||
const playbackDuration = bufferDuration / bufferSource.playbackRate.value;
|
||||
const sliceDuration = (end - begin) * playbackDuration;
|
||||
return { bufferSource, offset, bufferDuration, playbackDuration, sliceDuration };
|
||||
};
|
||||
|
||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||
|
||||
@@ -607,7 +607,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
|
||||
stretch !== undefined && chain.connect(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
||||
if (stretch !== undefined) {
|
||||
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch });
|
||||
chain.connect(phaseVocoder);
|
||||
fxNodes['stretch'] = [phaseVocoder];
|
||||
}
|
||||
|
||||
if (fx.transient !== undefined) {
|
||||
const transProcessor = getWorklet(
|
||||
@@ -821,6 +825,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
// panning
|
||||
if (fx.pan !== undefined) {
|
||||
const panner = ac.createStereoPanner();
|
||||
fxNodes['pan'] = [panner];
|
||||
panner.pan.value = 2 * fx.pan - 1;
|
||||
chain.connect(panner);
|
||||
}
|
||||
|
||||
@@ -42,4 +42,18 @@ describe('transpiler', () => {
|
||||
[12, 14],
|
||||
]);
|
||||
});
|
||||
it('allows disabling mini', () => {
|
||||
const code = `/* mini-off */
|
||||
const randPrefix = Math.random() > 0.5 ? "b" : "s";
|
||||
const drumPat = \`\${randPrefix}d\`;
|
||||
// mini-on
|
||||
s(drumPat).lpf("5000 10000") // make sure mini still runs;
|
||||
`;
|
||||
const { output, miniLocations } = transpiler(code, { ...simple, emitMiniLocations: true });
|
||||
expect(output).not.toContain("m('b'");
|
||||
expect(output).not.toContain("m('s'");
|
||||
const cutoffIdx = code.indexOf('5000 10000');
|
||||
expect(miniLocations).toHaveLength(2);
|
||||
expect(miniLocations[0][0]).toEqual(cutoffIdx);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,12 +21,15 @@ export function registerLanguage(type, config) {
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
|
||||
const comments = [];
|
||||
let ast = parse(input, {
|
||||
ecmaVersion: 2022,
|
||||
allowAwaitOutsideFunction: true,
|
||||
locations: true,
|
||||
onComment: comments,
|
||||
});
|
||||
|
||||
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const minilang = languages.get('minilang');
|
||||
@@ -66,6 +69,9 @@ export function transpiler(input, options = {}) {
|
||||
return this.replace(tidalWithLocation(raw, offset));
|
||||
}
|
||||
if (isBackTickString(node, parent)) {
|
||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||
return;
|
||||
}
|
||||
const { quasis } = node;
|
||||
const { raw } = quasis[0].value;
|
||||
this.skip();
|
||||
@@ -73,6 +79,9 @@ export function transpiler(input, options = {}) {
|
||||
return this.replace(miniWithLocation(raw, node));
|
||||
}
|
||||
if (isStringWithDoubleQuotes(node)) {
|
||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||
return;
|
||||
}
|
||||
const { value } = node;
|
||||
this.skip();
|
||||
emitMiniLocations && collectMiniLocations(value, node);
|
||||
@@ -327,3 +336,32 @@ function languageWithLocation(name, value, offset) {
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function findMiniDisableRanges(comments, codeEnd) {
|
||||
const ranges = [];
|
||||
const stack = []; // used to track on/off pairs
|
||||
for (const comment of comments) {
|
||||
const value = comment.value.trim();
|
||||
if (value.startsWith('mini-off')) {
|
||||
stack.push(comment.start);
|
||||
} else if (value.startsWith('mini-on')) {
|
||||
const start = stack.pop();
|
||||
ranges.push([start, comment.end]);
|
||||
}
|
||||
}
|
||||
while (stack.length) {
|
||||
// If no closing mini-on is found, just turn it off until `codeEnd`
|
||||
const start = stack.pop();
|
||||
ranges.push([start, codeEnd]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function isMiniDisabled(offset, miniDisableRanges) {
|
||||
for (const [start, end] of miniDisableRanges) {
|
||||
if (offset >= start && offset < end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ note("c3 [e3 g3]*2")
|
||||
is transpiled to:
|
||||
|
||||
```strudel
|
||||
note(m('c3 [e3 g3]', 5))
|
||||
note(m('c3 [e3 g3]*2', 5))
|
||||
```
|
||||
|
||||
Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later.
|
||||
|
||||
@@ -377,4 +377,4 @@ insect [crow metal] - -,
|
||||
punchcard
|
||||
/>
|
||||
|
||||
Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes)
|
||||
Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes).
|
||||
|
||||
Reference in New Issue
Block a user