Compare commits

...

17 Commits

Author SHA1 Message Date
Aria 6d73381985 More cleanup 2025-11-23 19:45:43 -06:00
Aria 7eb737006f Typo 2025-11-23 19:39:54 -06:00
Aria 8690f0e5d9 Bug fix 2025-11-23 19:17:44 -06:00
Aria 33ab0dc2c4 Working version 2025-11-23 19:14:24 -06:00
Aria ca6a976351 First pass at unifying phase vocoder with basic pitch shifter 2025-11-23 18:51:53 -06:00
Switch Angel AKA Jade Rose 132a124644 Merge pull request 'Add o as a synonym for orbit' (#1766) from jade/orbitsynonym into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1766
Reviewed-by: Aria <glossing@noreply.codeberg.org>
2025-11-24 01:20:52 +01:00
Jade (Rose) Rowland 8325b4cfc4 Merge branch 'main' into jade/orbitsynonym 2025-11-23 19:06:16 -05:00
Jade (Rose) Rowland 3b2df0b725 synonym: 2025-11-23 19:05:57 -05:00
froos 22f1512697 Merge pull request '[perf] fix connect-leak in fm modulation' (#1758) from jeromew/strudel:fix-perf6 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1758
2025-11-23 22:23:24 +01:00
froos 9a05a679aa Merge pull request 'Fix interoperability issue between all and await initHydra()' (#1663) from jeromew/strudel:jeromew-hydra-all into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1663
2025-11-23 09:47:22 +01:00
Aria f7f3aa7668 Merge pull request 'Bug fix: Swap l/r gains with temp variable' (#1768) from glossing/stereo-bug into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1768
2025-11-23 04:23:04 +01:00
Aria adc3a9ac6c Swap with temp variable 2025-11-22 20:51:30 -06:00
Jade (Rose) Rowland 2efd56e331 add synonym 2025-11-22 18:10:28 -05:00
jeromew d453c861d3 Merge branch 'fix-perf6' of https://codeberg.org/jeromew/strudel into fix-perf6 2025-11-21 09:01:02 +00:00
jeromew f637b8b0dd [perf] fix connect-leak in fm modulation 2025-11-21 09:00:17 +00:00
jeromew 907f03f3bf [perf] fix connect-leak in fm modulation 2025-11-20 17:40:51 +00:00
jeromew 47c85f8540 fix interoperability of all with hydra
Hydra add methods on Array.prototype which breaks the Array enumeration with for...in.
Use for...of instead
cf https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of#difference_between_for...of_and_for...in for details on the issue

without this fix, it is impossible to use both `await initHydra()` and `all` in the same strudel code. Strudel errors with "Error: got "undefined" instead of pattern."

example code: https://strudel.cc/#YXdhaXQgaW5pdEh5ZHJhKCkKCmFsbChwaWFub3JvbGwpCgokOiBzKCJiZCIp
2025-10-16 14:06:33 +02:00
5 changed files with 104 additions and 31 deletions
+4 -1
View File
@@ -1833,6 +1833,7 @@ export const { octave } = registerControl('octave');
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
*
* @name orbit
* @synonyms o
* @param {number | Pattern} number
* @example
* stack(
@@ -1840,7 +1841,7 @@ export const { octave } = registerControl('octave');
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
export const { orbit } = registerControl('orbit');
export const { orbit } = registerControl('orbit', 'o');
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
export const { overgain } = registerControl('overgain');
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
@@ -2169,6 +2170,8 @@ export const { speed } = registerControl('speed');
*
*/
export const { stretch } = registerControl('stretch');
export const { pshift } = registerControl('pshift');
/**
* Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
*
+2 -2
View File
@@ -238,8 +238,8 @@ export function repl({
pattern = eachTransform(pattern);
}
if (allTransforms.length) {
for (let i in allTransforms) {
pattern = allTransforms[i](pattern);
for (const transform of allTransforms) {
pattern = transform(pattern);
}
}
+7 -2
View File
@@ -362,9 +362,9 @@ const mod = (freq, range = 1, type = 'sine') => {
}
osc.start();
const g = new GainNode(ctx, { gain: range });
const g = gainNode(range);
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
return { node: g, stop: (t) => osc.stop(t), osc: osc };
};
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = frequencyparam.value;
@@ -416,6 +416,11 @@ export function applyFM(param, value, begin) {
modulator.connect(envGain);
envGain.connect(param);
}
fmmod.osc.onended = () => {
envGain.disconnect();
modulator.disconnect();
fmmod.osc.disconnect();
};
}
return { stop };
}
+9 -1
View File
@@ -451,6 +451,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
compressorKnee,
compressorAttack,
compressorRelease,
pshift,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -530,7 +531,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
stretch !== undefined &&
chain.push(
getWorklet(ac, 'pitch-processor', { pitchFactor: stretch }, { processorOptions: { vocoderMode: true } }),
);
pshift !== undefined &&
chain.push(
getWorklet(ac, 'pitch-processor', { pitchFactor: pshift }, { processorOptions: { vocoderMode: false } }),
);
// gain stage
chain.push(gainNode(gain));
+82 -25
View File
@@ -543,8 +543,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
if (pn >= 1.0) pn -= 1.0;
this.phase[n] = pn;
// invert right and left gain
const tmp = gainL;
gainL = gainR;
gainR = gainL;
gainR = tmp;
}
}
return true;
@@ -568,57 +569,79 @@ function genHannWindow(length) {
return hannCache.get(length);
}
class PhaseVocoderProcessor extends OLAProcessor {
class PitchProcessor extends OLAProcessor {
static get parameterDescriptors() {
return [
{
name: 'pitchFactor',
defaultValue: 1.0,
defaultValue: 1,
},
];
}
constructor(options) {
options.processorOptions = {
blockSize: BUFFERED_BLOCK_SIZE,
const parentOptions = {
...options,
processorOptions: {
blockSize: BUFFERED_BLOCK_SIZE,
},
};
super(options);
this.timeCursor = 0;
super(parentOptions);
// if true, use spectral peak-finding to cluster strong bins ('stretch')
// else, use simple shift & interpolate ('pitch')
this.vocoderMode = options.processorOptions.vocoderMode ?? true;
this.fftSize = this.blockSize;
this.invfftSize = 1 / this.fftSize;
this.hannWindow = genHannWindow(this.fftSize);
// prepare FFT and pre-allocate buffers
this.nyquistBin = this.fftSize / 2;
this.hannWindow = genHannWindow(this.blockSize);
this.fft = new FFT(this.fftSize);
this.freqComplexBuffer = this.fft.createComplexArray();
this.freqComplexBufferShifted = this.fft.createComplexArray();
this.timeComplexBuffer = this.fft.createComplexArray();
this.magnitudes = new Float32Array(this.fftSize / 2 + 1);
this.peakIndexes = new Int32Array(this.magnitudes.length);
this.nbPeaks = 0;
this.timeCursor = 0;
// for peak tracking in phase vocoder mode
if (this.vocoderMode) {
this.magnitudes = new Float32Array(this.fftSize / 2 + 1);
this.peakIndexes = new Int32Array(this.magnitudes.length);
this.nbPeaks = 0;
}
}
processOLA(inputs, outputs, parameters) {
// no automation, take last value
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
if (pitchFactor < 0) {
pitchFactor = pitchFactor * 0.25;
if (this.vocoderMode) {
pitchFactor = pitchFactor < 0 ? pitchFactor * 0.25 : pitchFactor;
pitchFactor += 1;
}
pitchFactor = Math.max(0, pitchFactor + 1);
pitchFactor = Math.max(0.01, pitchFactor);
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < inputs[i].length; j++) {
const input = inputs[i][j];
const output = outputs[i][j];
this.applyHannWindow(input);
this.fft.realTransform(this.freqComplexBuffer, input);
this.computeMagnitudes();
this.findPeaks();
this.shiftPeaks(pitchFactor);
if (this.vocoderMode) {
this.computeMagnitudes();
this.findPeaks();
this.shiftPeaks(pitchFactor);
} else {
this.shiftSpectrum(pitchFactor);
}
this.fft.completeSpectrum(this.freqComplexBufferShifted);
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
this.fft.fromComplexArray(this.timeComplexBuffer, output);
this.applyHannWindow(output);
}
}
this.timeCursor += this.hopSize;
}
@@ -629,6 +652,47 @@ class PhaseVocoderProcessor extends OLAProcessor {
}
}
writeShiftedBin(destBin, valueReal, valueImag, phaseShiftReal, phaseShiftImag, accumulate) {
const shiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
const shiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
const destIndex = destBin * 2;
if (accumulate) {
this.freqComplexBufferShifted[destIndex] += shiftedReal;
this.freqComplexBufferShifted[destIndex + 1] += shiftedImag;
} else {
this.freqComplexBufferShifted[destIndex] = shiftedReal;
this.freqComplexBufferShifted[destIndex + 1] = shiftedImag;
}
}
/** Shift entire spectrum with simple resampling */
shiftSpectrum(pitchFactor) {
// zero-fill new spectrum
this.freqComplexBufferShifted.fill(0);
const nyquist = this.nyquistBin;
for (let destBin = 0; destBin <= nyquist; destBin++) {
const sourceBin = destBin / pitchFactor;
if (sourceBin > nyquist) {
break;
}
const lower = ffloor(sourceBin);
const upper = lower + 1 > nyquist ? nyquist : lower + 1;
const t = sourceBin - lower;
const lowerIndex = lower * 2;
const upperIndex = upper * 2;
const realLower = this.freqComplexBuffer[lowerIndex];
const imagLower = this.freqComplexBuffer[lowerIndex + 1];
const realUpper = this.freqComplexBuffer[upperIndex];
const imagUpper = this.freqComplexBuffer[upperIndex + 1];
const real = lerp(realLower, realUpper, t);
const imag = lerp(imagLower, imagUpper, t);
const omegaDelta = TWO_PI * this.invfftSize * (destBin - sourceBin);
const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
this.writeShiftedBin(destBin, real, imag, phaseShiftReal, phaseShiftImag, false);
}
}
/** Compute squared magnitudes for peak finding **/
computeMagnitudes() {
let i = 0,
@@ -700,20 +764,13 @@ class PhaseVocoderProcessor extends OLAProcessor {
const indexImag = indexReal + 1;
const valueReal = this.freqComplexBuffer[indexReal];
const valueImag = this.freqComplexBuffer[indexImag];
const valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
const valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
const indexShiftedReal = 2 * binIndexShifted;
const indexShiftedImag = indexShiftedReal + 1;
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
this.writeShiftedBin(binIndexShifted, valueReal, valueImag, phaseShiftReal, phaseShiftImag, true);
}
}
}
}
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
registerProcessor('pitch-processor', PitchProcessor);
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
class PulseOscillatorProcessor extends AudioWorkletProcessor {