mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-19 04:06:16 -04:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d73381985 | |||
| 7eb737006f | |||
| 8690f0e5d9 | |||
| 33ab0dc2c4 | |||
| ca6a976351 | |||
| 132a124644 | |||
| 8325b4cfc4 | |||
| 3b2df0b725 | |||
| 22f1512697 | |||
| 9a05a679aa | |||
| f7f3aa7668 | |||
| adc3a9ac6c | |||
| 2518b2aed2 | |||
| 1d5f3a4f30 | |||
| 2efd56e331 | |||
| 5fa6cb4653 | |||
| 19fd0fc649 | |||
| e72e26eb81 | |||
| 1c4b05d55f | |||
| d453c861d3 | |||
| f637b8b0dd | |||
| 907f03f3bf | |||
| 47c85f8540 | |||
| f4cf77f5c6 |
@@ -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`.
|
||||
*
|
||||
|
||||
+14
-12
@@ -215,18 +215,20 @@ export function repl({
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = [];
|
||||
let soloActive = false;
|
||||
for (const [key, value] of Object.entries(pPatterns)) {
|
||||
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||
// handle soloed patterns ex: S$: s("bd!4")
|
||||
const isSolod = key.length > 1 && key.startsWith('S');
|
||||
if (isSolod && soloActive === false) {
|
||||
// first time we see a soloed pattern, clear existing patterns
|
||||
patterns = [];
|
||||
soloActive = true;
|
||||
}
|
||||
if (!soloActive || (soloActive && isSolod)) {
|
||||
const valWithState = value.withState((state) => state.setControls({ id: key }));
|
||||
patterns.push(valWithState);
|
||||
}
|
||||
}
|
||||
|
||||
// if there are solo patterns, only use those
|
||||
const soloPatterns = Object.entries(pPatterns).filter(([key]) => key.length > 1 && key.startsWith('S'));
|
||||
if (soloPatterns.length) {
|
||||
patterns = Object.values(Object.fromEntries(soloPatterns));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
patterns = patterns.map((x) => eachTransform(x));
|
||||
@@ -236,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ export function createFilter(context, start, end, params, cps) {
|
||||
if (sync != null) {
|
||||
rate = cps * sync;
|
||||
}
|
||||
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
|
||||
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate, min: 10, max: 20000 };
|
||||
getParamLfo(context, frequencyParam, start, end, lfoValues);
|
||||
return filter;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,13 +4,13 @@ export default function UdelsHeader(Props) {
|
||||
const { numWindows, setNumWindows } = Props;
|
||||
|
||||
return (
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-black">
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-800">
|
||||
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
|
||||
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
|
||||
<div className={'mt-[1px] cursor-pointer'}>🦄</div>
|
||||
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
|
||||
|
||||
<div>
|
||||
<span className="font-mono text-">SWITCH ANGEL</span>
|
||||
<div className={'animate-pulse'}>
|
||||
<span className="">strudel</span> <span className="text-sm">-UDELS</span>
|
||||
</div>
|
||||
</h1>
|
||||
<NumberInput value={numWindows} setValue={setNumWindows} />
|
||||
|
||||
@@ -47,12 +47,12 @@ export function Header({ context, embedded = false }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="block text-foreground ">*</span>
|
||||
<span className="block text-foreground rotate-90">꩜</span>
|
||||
</div>
|
||||
{!isZen && (
|
||||
<div className="space-x-2">
|
||||
<span className="">Switch Angel</span>
|
||||
<span className="text-sm font-medium"></span>
|
||||
<span className="">strudel</span>
|
||||
<span className="text-sm font-medium">REPL</span>
|
||||
{!isEmbedded && isButtonRowHidden && (
|
||||
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
|
||||
DOCS
|
||||
@@ -82,7 +82,47 @@ export function Header({ context, embedded = false }) {
|
||||
<>loading...</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleEvaluate}
|
||||
title="update"
|
||||
className={cx(
|
||||
'flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
|
||||
)}
|
||||
>
|
||||
{!isEmbedded && <span>update</span>}
|
||||
</button>
|
||||
{/* !isEmbedded && (
|
||||
<button
|
||||
title="shuffle"
|
||||
className="hover:opacity-50 p-2 flex items-center space-x-1"
|
||||
onClick={handleShuffle}
|
||||
>
|
||||
<span> shuffle</span>
|
||||
</button>
|
||||
) */}
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
title="share"
|
||||
className={cx(
|
||||
'cursor-pointer hover:opacity-50 flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
)}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<span>share</span>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<a
|
||||
title="learn"
|
||||
href={`${baseNoTrailing}/workshop/getting-started/`}
|
||||
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
{/* {isEmbedded && (
|
||||
<button className={cx('hover:opacity-50 px-2')}>
|
||||
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
||||
|
||||
@@ -79,7 +79,6 @@ const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
|
||||
function UserPatterns({ context }) {
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
|
||||
@@ -36,7 +36,6 @@ import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
|
||||
import './Repl.css';
|
||||
import { setInterval, clearInterval } from 'worker-timers';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
import jadeScriptsRaw from '../../../../../../switchangel/strudel-scripts/allscripts.js?raw'
|
||||
|
||||
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
|
||||
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
|
||||
@@ -143,10 +142,6 @@ export function useReplContext() {
|
||||
msg = `Default code has been loaded`;
|
||||
}
|
||||
editor.setCode(code);
|
||||
// console.info(jadeScriptsRaw)
|
||||
editor.repl.evaluate(jadeScriptsRaw)
|
||||
|
||||
|
||||
setDocumentTitle(code);
|
||||
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user