mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-19 04:06:16 -04:00
Compare commits
41 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 | |||
| 06bf17f444 | |||
| 21543956c6 | |||
| 4758effdc6 | |||
| 6f58e74bea | |||
| 907f03f3bf | |||
| 0435711051 | |||
| 8f6718b9e8 | |||
| aa77255d98 | |||
| 7267990e20 | |||
| 387e5db0fe | |||
| d9f63b8dfa | |||
| 8ed72b4573 | |||
| dd2c808cd7 | |||
| 389a63a850 | |||
| 99beb4454d | |||
| 30cc46ea66 | |||
| bfa8fa3c75 | |||
| 47c85f8540 | |||
| f4cf77f5c6 | |||
| 7f103e77e3 |
@@ -1622,40 +1622,6 @@ export const { unison } = registerControl('unison');
|
||||
*
|
||||
*/
|
||||
export const { spread } = registerControl('spread');
|
||||
|
||||
/**
|
||||
* Controls how much the detune is concentrated outwards or inwards. A value of 0 will be
|
||||
* linear spacing of voices; -1 will be concentrated outwards (inverse power law); 1 will be concentrated inwards
|
||||
*
|
||||
* @name dpow
|
||||
* @synonyms detunepower
|
||||
* @param {number | Pattern} power between -1 and 1
|
||||
*
|
||||
*/
|
||||
export const { dpow, detunepower } = registerControl('dpow', 'detunepower');
|
||||
|
||||
/**
|
||||
* Controls how much the central voices are emphasized in a detuned oscillator. -1 will emphasize detuned voices;
|
||||
* 1 will emphasize central voices (i.e. either the raw frequency if `unison` is odd or the central two detuned frequencies
|
||||
* if it is even); 0 will be the default blending
|
||||
*
|
||||
* @name dblend
|
||||
* @synonyms detuneblend
|
||||
* @param {number | Pattern} blend between -1 and 1
|
||||
*
|
||||
*/
|
||||
export const { dblend, detuneblend } = registerControl('dblend', 'detuneblend');
|
||||
|
||||
/**
|
||||
* Sets the stacking mode of detuned oscillators
|
||||
*
|
||||
* @name dstack
|
||||
* @synonyms detunestack
|
||||
* @param {number | Pattern} mode mode index starting at 0
|
||||
*
|
||||
*/
|
||||
export const { dstack, detunestack } = registerControl('dstack', 'detunestack');
|
||||
|
||||
/**
|
||||
* Set dryness of reverb. See `room` and `size` for more information about reverb.
|
||||
*
|
||||
@@ -1867,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(
|
||||
@@ -1874,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
|
||||
@@ -2203,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`.
|
||||
*
|
||||
|
||||
+16
-5
@@ -152,9 +152,9 @@ export function repl({
|
||||
// allows muting a pattern x with x_ or _x
|
||||
return silence;
|
||||
}
|
||||
if (id === '$') {
|
||||
if (id.includes('$')) {
|
||||
// allows adding anonymous patterns with $:
|
||||
id = `$${anonymousIndex}`;
|
||||
id = `${id}${anonymousIndex}`;
|
||||
anonymousIndex++;
|
||||
}
|
||||
pPatterns[id] = this;
|
||||
@@ -215,8 +215,19 @@ 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 (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
@@ -227,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
* @example
|
||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||
* @example
|
||||
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
||||
* wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note()
|
||||
* @example
|
||||
* // The probability can itself be a pattern
|
||||
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { clamp, midiToFreq, nanFallback, noteToMidi } from './util.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -262,7 +262,15 @@ export function drywet(dry, wet, wetAmount = 0) {
|
||||
let mix = ac.createGain();
|
||||
dry_gain.connect(mix);
|
||||
wet_gain.connect(mix);
|
||||
return mix;
|
||||
return {
|
||||
node: mix,
|
||||
onended: () => {
|
||||
dry_gain.disconnect(mix);
|
||||
wet_gain.disconnect(mix);
|
||||
dry.disconnect(dry_gain);
|
||||
wet.disconnect(wet_gain);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let curves = ['linear', 'exponential'];
|
||||
@@ -297,6 +305,10 @@ export function getVibratoOscillator(param, value, t) {
|
||||
gain.gain.value = vibmod * 100;
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(param);
|
||||
vibratoOscillator.onended = () => {
|
||||
gain.disconnect(param);
|
||||
vibratoOscillator.disconnect(gain);
|
||||
};
|
||||
vibratoOscillator.start(t);
|
||||
return vibratoOscillator;
|
||||
}
|
||||
@@ -350,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;
|
||||
@@ -404,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 };
|
||||
}
|
||||
@@ -525,23 +542,3 @@ export const destroyAudioWorkletNode = (node) => {
|
||||
node.disconnect();
|
||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||
};
|
||||
|
||||
export const getDetuner = (detune = 0, power = 0) => {
|
||||
if (detune <= 0) {
|
||||
return () => 0;
|
||||
}
|
||||
const curve = (x) => {
|
||||
if (power === 0) {
|
||||
return x;
|
||||
}
|
||||
const a = Math.abs(x),
|
||||
s = Math.sign(x);
|
||||
if (power > 0) {
|
||||
return s * Math.pow(a, 1 + 4 * power);
|
||||
}
|
||||
return s * (1 - Math.pow(1 - a, 1 - 4 * power));
|
||||
};
|
||||
return (t) => {
|
||||
return curve(2 * t - 1) * detune;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,8 +65,9 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
|
||||
export function getNoiseMix(inputNode, wet, t) {
|
||||
const noiseOscillator = getNoiseOscillator('pink', t);
|
||||
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
||||
noiseOscillator.node.onended = () => noiseMix.onended();
|
||||
return {
|
||||
node: noiseMix,
|
||||
node: noiseMix.node,
|
||||
stop: (time) => noiseOscillator?.stop(time),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
@@ -692,7 +700,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
// delay
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||
orbitBus.sendDelay(post, delay);
|
||||
const send = orbitBus.sendDelay(post, delay);
|
||||
audioNodes.push(send);
|
||||
}
|
||||
// reverb
|
||||
if (room > 0) {
|
||||
|
||||
@@ -82,7 +82,7 @@ export class Orbit {
|
||||
}
|
||||
|
||||
sendDelay(node, amount) {
|
||||
effectSend(node, this.delayNode, amount);
|
||||
return effectSend(node, this.delayNode, amount);
|
||||
}
|
||||
|
||||
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||
|
||||
@@ -48,19 +48,17 @@ export function registerSynthSounds() {
|
||||
[0.001, 0.05, 0.6, 0.01],
|
||||
);
|
||||
|
||||
let sound = getOscillator(s, t, value);
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
let sound = getOscillator(s, t, value, () => {
|
||||
g.disconnect();
|
||||
onended();
|
||||
};
|
||||
});
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
@@ -174,25 +172,22 @@ export function registerSynthSounds() {
|
||||
begin,
|
||||
end,
|
||||
freqspread: detune,
|
||||
voices,
|
||||
panspread,
|
||||
power: value.dpow,
|
||||
blend: value.dblend,
|
||||
},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
processorOptions: {
|
||||
voices,
|
||||
stackmode: value.dstack,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3, begin, holdend, 'linear');
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
||||
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
@@ -463,7 +458,7 @@ export function waveformN(partials, phases, type) {
|
||||
}
|
||||
|
||||
// expects one of waveforms as s
|
||||
export function getOscillator(s, t, value) {
|
||||
export function getOscillator(s, t, value, onended) {
|
||||
const { duration, noise = 0 } = value;
|
||||
const partials = value.partials ?? value.n;
|
||||
let o;
|
||||
@@ -485,7 +480,6 @@ export function getOscillator(s, t, value) {
|
||||
}
|
||||
// set frequency
|
||||
o.frequency.value = getFrequencyFromValue(value);
|
||||
o.start(t);
|
||||
|
||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
||||
|
||||
@@ -498,6 +492,13 @@ export function getOscillator(s, t, value) {
|
||||
noiseMix = getNoiseMix(o, noise, t);
|
||||
}
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
noiseMix?.node.disconnect();
|
||||
onended();
|
||||
};
|
||||
o.start(t);
|
||||
|
||||
return {
|
||||
node: noiseMix?.node || o,
|
||||
stop: (time) => {
|
||||
|
||||
@@ -234,19 +234,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
freqspread: value.detune,
|
||||
position: value.wt,
|
||||
warp: value.warp,
|
||||
warpMode: warpmode,
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
panspread: value.spread,
|
||||
power: value.dpow,
|
||||
blend: value.dblend,
|
||||
},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
processorOptions: {
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
stackmode: value.dstack,
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
warpmode,
|
||||
},
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
},
|
||||
{ outputChannelCount: [2] },
|
||||
);
|
||||
source.port.postMessage({ type: 'table', payload });
|
||||
if (ac.currentTime > t) {
|
||||
|
||||
+177
-159
@@ -4,7 +4,7 @@
|
||||
|
||||
import OLAProcessor from './ola-processor';
|
||||
import FFT from './fft.js';
|
||||
import { getDistortionAlgorithm, getDetuner } from './helpers.mjs';
|
||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||
|
||||
const blockSize = 128;
|
||||
const PI = Math.PI;
|
||||
@@ -28,6 +28,16 @@ const fast_tanh = (x) => {
|
||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
||||
};
|
||||
|
||||
// Optimized per-voice detuner which precomputes constants
|
||||
const getDetuner = (unison, detune) => {
|
||||
if (unison < 2) {
|
||||
return (_voiceIdx) => 0;
|
||||
}
|
||||
const scale = detune / (unison - 1);
|
||||
const center = detune * 0.5;
|
||||
return (voiceIdx) => voiceIdx * scale - center;
|
||||
};
|
||||
|
||||
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
||||
return frequency * Math.pow(2, detune / 12);
|
||||
};
|
||||
@@ -444,68 +454,11 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
registerProcessor('distort-processor', DistortProcessor);
|
||||
|
||||
const INTERVALS = [
|
||||
[1],
|
||||
[1, 2],
|
||||
[1, 3],
|
||||
[1, 2, 4],
|
||||
[1, 2, 4, 8],
|
||||
[1, 2, 3],
|
||||
[1, 1.5, 2.4],
|
||||
[1, 2.5],
|
||||
[1, 1.782],
|
||||
[1, 2.4],
|
||||
[1, 1.5, 2.5],
|
||||
[1, 1.667],
|
||||
[1, 2.25],
|
||||
];
|
||||
const _getIntervals = (mode = 0) => {
|
||||
return INTERVALS[mode] ?? INTERVALS[0];
|
||||
};
|
||||
|
||||
const initUnisonProcessor = (processor, processorOptions) => {
|
||||
processor.intervals = _getIntervals(processorOptions.stackmode);
|
||||
processor.intervalGains = processor.intervals.map((i) => 1 / i ** 0.5); // reduce volume of upper harmonics
|
||||
processor.intNorm = Math.sqrt(1 / processor.intervalGains.reduce((acc, i) => acc + i ** 2, 0));
|
||||
processor.numIntervals = processor.intervals.length;
|
||||
processor.voices = processorOptions.voices;
|
||||
processor.isDetuned = processor.voices > 1;
|
||||
processor.isCenter = (idx) => {
|
||||
if (!processor.isDetuned) return true;
|
||||
if (processor.voices % 2 === 1) {
|
||||
return idx === (processor.voices - 1) >> 1;
|
||||
} else {
|
||||
const right = processor.voices >> 1;
|
||||
const left = right - 1;
|
||||
return idx === left || idx === right;
|
||||
}
|
||||
};
|
||||
const totalVoices = processor.voices * processor.numIntervals;
|
||||
processor.blendGains = new Array(processor.voices).fill(1);
|
||||
const phaserand = processorOptions.phaserand ?? 1;
|
||||
processor.phases = new Array(totalVoices).fill(0).map(() => Math.random() * phaserand);
|
||||
return totalVoices;
|
||||
};
|
||||
|
||||
const getUnisonData = (processor, params, i) => {
|
||||
const detune = pv(params.detune, i);
|
||||
const freqspread = pv(params.freqspread, i);
|
||||
const blend = pv(params.blend, i) * 0.95;
|
||||
const blendGains = processor.blendGains.map((_, idx) => (processor.isCenter(idx) ? 1 - blend : 1 + blend));
|
||||
const blendNorm = 1 / Math.sqrt(blendGains.reduce((acc, g) => acc + g ** 2, 0));
|
||||
const normalizer = blendNorm * processor.intNorm;
|
||||
const power = pv(params.power, i);
|
||||
const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
|
||||
const detuner = getDetuner(freqspread, power);
|
||||
const inv = processor.voices > 1 ? 1 / (processor.voices - 1) : 0;
|
||||
return { blendGains, normalizer, detuner, inv, freq };
|
||||
};
|
||||
|
||||
// SUPERSAW
|
||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor({ processorOptions }) {
|
||||
constructor() {
|
||||
super();
|
||||
initUnisonProcessor(this, processorOptions);
|
||||
this.phase = [];
|
||||
}
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
@@ -515,17 +468,20 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'frequency',
|
||||
defaultValue: 440,
|
||||
min: Number.EPSILON,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'panspread',
|
||||
defaultValue: 0.4,
|
||||
@@ -542,17 +498,12 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
defaultValue: 0,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'blend',
|
||||
defaultValue: 0,
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
{
|
||||
name: 'power',
|
||||
defaultValue: 0,
|
||||
min: -1,
|
||||
max: 1,
|
||||
name: 'voices',
|
||||
defaultValue: 5,
|
||||
min: 1,
|
||||
automationRate: 'k-rate',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -561,35 +512,40 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
return true;
|
||||
}
|
||||
if (currentTime >= params.end[0]) {
|
||||
// this.port.postMessage({ type: 'onended' });
|
||||
return false;
|
||||
}
|
||||
const output = outputs[0];
|
||||
const voices = params.voices[0]; // k-rate
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
const { blendGains, normalizer, detuner, inv, freq } = getUnisonData(this, params, i);
|
||||
const detune = pv(params.detune, i);
|
||||
const freqspread = pv(params.freqspread, i);
|
||||
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
||||
let gainL = Math.sqrt(1 - panspread) * normalizer;
|
||||
let gainR = Math.sqrt(panspread) * normalizer;
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
let gainL = Math.sqrt(1 - panspread);
|
||||
let gainR = Math.sqrt(panspread);
|
||||
let freq = pv(params.frequency, i);
|
||||
// Main detuning
|
||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||
const detuner = getDetuner(voices, freqspread);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
// Individual voice detuning
|
||||
const freqVoice = this.isDetuned ? applySemitoneDetuneToFrequency(freq, detuner(n * inv)) : freq; // voice detune
|
||||
const bG = blendGains[n];
|
||||
for (let idx = 0; idx < this.numIntervals; idx++) {
|
||||
const g = bG * this.intervalGains[idx];
|
||||
const freqStack = freqVoice * this.intervals[idx];
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
// has domain [0, 1]
|
||||
const dt = ffrac(freqStack * INVSR);
|
||||
const voiceNum = n * this.numIntervals + idx;
|
||||
const v = waveshapes.sawblep(this.phases[voiceNum], dt);
|
||||
output[0][i] += v * gainL * g;
|
||||
output[1][i] += v * gainR * g;
|
||||
let pn = this.phases[voiceNum] + dt;
|
||||
if (pn >= 1.0) pn -= 1.0;
|
||||
this.phases[voiceNum] = pn;
|
||||
// invert right and left gain
|
||||
gainL = gainR;
|
||||
gainR = gainL;
|
||||
}
|
||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
// has domain [0, 1]
|
||||
const dt = frac(freqVoice * INVSR);
|
||||
this.phase[n] = this.phase[n] ?? Math.random();
|
||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||
|
||||
output[0][i] += v * gainL;
|
||||
output[1][i] += v * gainR;
|
||||
|
||||
let pn = this.phase[n] + dt;
|
||||
if (pn >= 1.0) pn -= 1.0;
|
||||
this.phase[n] = pn;
|
||||
// invert right and left gain
|
||||
const tmp = gainL;
|
||||
gainL = gainR;
|
||||
gainR = tmp;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -613,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;
|
||||
}
|
||||
|
||||
@@ -674,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,
|
||||
@@ -745,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 {
|
||||
@@ -1169,24 +1181,24 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warpMode', defaultValue: 0 },
|
||||
{ name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
|
||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||
{ name: 'blend', defaultValue: 0, min: -1, max: 1 },
|
||||
{ name: 'power', defaultValue: 0, min: -1, max: 1 },
|
||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
constructor({ processorOptions }) {
|
||||
super();
|
||||
initUnisonProcessor(this, processorOptions);
|
||||
this.warpmode = processorOptions.warpmode;
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.frameLen = 0;
|
||||
this.numFrames = 0;
|
||||
this.phase = [];
|
||||
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'table') {
|
||||
const key = payload.key;
|
||||
this.frameLen = payload.frameLen;
|
||||
this.baseThreshold = this.frameLen >>> 3; // used for mipmaps
|
||||
if (!tablesCache[key]) {
|
||||
const tables = [payload.frames];
|
||||
let table = tables[0];
|
||||
@@ -1354,13 +1366,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
_chooseMip(dphi) {
|
||||
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
||||
const numTables = this.tables.length;
|
||||
let level = 0,
|
||||
threshold = this.baseThreshold;
|
||||
while (level + 1 < numTables && approxHarm < threshold) {
|
||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
||||
let level = 0;
|
||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||
level++;
|
||||
threshold *= 2;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
@@ -1379,41 +1388,50 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
if (outR !== outL) outR.set(outL);
|
||||
return true;
|
||||
}
|
||||
const voices = parameters.voices[0]; // k-rate
|
||||
for (let i = 0; i < outL.length; i++) {
|
||||
const { blendGains, normalizer, detuner, inv, freq } = getUnisonData(this, parameters, i);
|
||||
const detune = pv(parameters.detune, i);
|
||||
const freqspread = pv(parameters.freqspread, i);
|
||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||
const idx = tablePos * (this.numFrames - 1);
|
||||
const fIdx = idx | 0;
|
||||
const frac = idx - fIdx;
|
||||
const interpT = idx - fIdx;
|
||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||
const panspread = this.isDetuned ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||
let gainL = Math.sqrt(0.5 - 0.5 * panspread) * normalizer;
|
||||
let gainR = Math.sqrt(0.5 + 0.5 * panspread) * normalizer;
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
const fVoice = this.isDetuned ? applySemitoneDetuneToFrequency(freq, detuner(n * inv)) : freq; // voice detune
|
||||
const bG = blendGains[n];
|
||||
for (let idx = 0; idx < this.numIntervals; idx++) {
|
||||
const g = bG * this.intervalGains[idx];
|
||||
const fStack = fVoice * this.intervals[idx];
|
||||
const dt = fStack * INVSR;
|
||||
const voiceNum = n * this.numIntervals + idx;
|
||||
const level = this._chooseMip(dt);
|
||||
const table = this.tables[level];
|
||||
// warp phase then sample
|
||||
const ph = this._warpPhase(this.phases[voiceNum], warpAmount, this.warpmode);
|
||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||
let s = s0 + (s1 - s0) * frac;
|
||||
if (this.warpmode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||
s = -s;
|
||||
}
|
||||
outL[i] += s * gainL * g;
|
||||
outR[i] += s * gainR * g;
|
||||
this.phases[voiceNum] = ffrac(this.phases[voiceNum] + dt);
|
||||
// invert right and left gain
|
||||
gainL = gainR;
|
||||
gainR = gainL;
|
||||
const warpMode = pv(parameters.warpMode, i);
|
||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||
let f = pv(parameters.frequency, i);
|
||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||
const normalizer = 1 / Math.sqrt(voices);
|
||||
const detuner = getDetuner(voices, freqspread);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const isOdd = (n & 1) == 1;
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
if (isOdd) {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
||||
const dPhase = fVoice * INVSR;
|
||||
const level = this._chooseMip(dPhase);
|
||||
const table = this.tables[level];
|
||||
|
||||
// warp phase then sample
|
||||
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||
let s = lerp(s0, s1, interpT);
|
||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||
s = -s;
|
||||
}
|
||||
outL[i] += s * gainL * normalizer;
|
||||
outR[i] += s * gainR * normalizer;
|
||||
this.phase[n] = frac(this.phase[n] + dPhase);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -12484,54 +12484,54 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/12 | s:bd ]",
|
||||
"[ 1/12 → 1/6 | s:bd ]",
|
||||
"[ 1/6 → 1/4 | s:bd ]",
|
||||
"[ 1/4 → 1/3 | s:bd ]",
|
||||
"[ 1/3 → 5/12 | s:bd ]",
|
||||
"[ 5/12 → 1/2 | s:bd ]",
|
||||
"[ 1/2 → 7/12 | s:sd ]",
|
||||
"[ 7/12 → 2/3 | s:sd ]",
|
||||
"[ 2/3 → 3/4 | s:sd ]",
|
||||
"[ 3/4 → 5/6 | s:bd ]",
|
||||
"[ 5/6 → 11/12 | s:bd ]",
|
||||
"[ 11/12 → 1/1 | s:bd ]",
|
||||
"[ 1/1 → 13/12 | s:bd ]",
|
||||
"[ 13/12 → 7/6 | s:bd ]",
|
||||
"[ 7/6 → 5/4 | s:bd ]",
|
||||
"[ 5/4 → 4/3 | s:bd ]",
|
||||
"[ 4/3 → 17/12 | s:bd ]",
|
||||
"[ 17/12 → 3/2 | s:bd ]",
|
||||
"[ 3/2 → 19/12 | s:hh ]",
|
||||
"[ 19/12 → 5/3 | s:hh ]",
|
||||
"[ 5/3 → 7/4 | s:hh ]",
|
||||
"[ 7/4 → 11/6 | s:bd ]",
|
||||
"[ 11/6 → 23/12 | s:bd ]",
|
||||
"[ 23/12 → 2/1 | s:bd ]",
|
||||
"[ 2/1 → 25/12 | s:hh ]",
|
||||
"[ 25/12 → 13/6 | s:hh ]",
|
||||
"[ 13/6 → 9/4 | s:hh ]",
|
||||
"[ 9/4 → 7/3 | s:hh ]",
|
||||
"[ 7/3 → 29/12 | s:hh ]",
|
||||
"[ 29/12 → 5/2 | s:hh ]",
|
||||
"[ 5/2 → 31/12 | s:bd ]",
|
||||
"[ 31/12 → 8/3 | s:bd ]",
|
||||
"[ 8/3 → 11/4 | s:bd ]",
|
||||
"[ 11/4 → 17/6 | s:bd ]",
|
||||
"[ 17/6 → 35/12 | s:bd ]",
|
||||
"[ 35/12 → 3/1 | s:bd ]",
|
||||
"[ 3/1 → 37/12 | s:bd ]",
|
||||
"[ 37/12 → 19/6 | s:bd ]",
|
||||
"[ 19/6 → 13/4 | s:bd ]",
|
||||
"[ 13/4 → 10/3 | s:sd ]",
|
||||
"[ 10/3 → 41/12 | s:sd ]",
|
||||
"[ 41/12 → 7/2 | s:sd ]",
|
||||
"[ 7/2 → 43/12 | s:hh ]",
|
||||
"[ 43/12 → 11/3 | s:hh ]",
|
||||
"[ 11/3 → 15/4 | s:hh ]",
|
||||
"[ 15/4 → 23/6 | s:bd ]",
|
||||
"[ 23/6 → 47/12 | s:bd ]",
|
||||
"[ 47/12 → 4/1 | s:bd ]",
|
||||
"[ 0/1 → 1/12 | note:c ]",
|
||||
"[ 1/12 → 1/6 | note:c ]",
|
||||
"[ 1/6 → 1/4 | note:c ]",
|
||||
"[ 1/4 → 1/3 | note:c ]",
|
||||
"[ 1/3 → 5/12 | note:c ]",
|
||||
"[ 5/12 → 1/2 | note:c ]",
|
||||
"[ 1/2 → 7/12 | note:f ]",
|
||||
"[ 7/12 → 2/3 | note:f ]",
|
||||
"[ 2/3 → 3/4 | note:f ]",
|
||||
"[ 3/4 → 5/6 | note:c ]",
|
||||
"[ 5/6 → 11/12 | note:c ]",
|
||||
"[ 11/12 → 1/1 | note:c ]",
|
||||
"[ 1/1 → 13/12 | note:c ]",
|
||||
"[ 13/12 → 7/6 | note:c ]",
|
||||
"[ 7/6 → 5/4 | note:c ]",
|
||||
"[ 5/4 → 4/3 | note:c ]",
|
||||
"[ 4/3 → 17/12 | note:c ]",
|
||||
"[ 17/12 → 3/2 | note:c ]",
|
||||
"[ 3/2 → 19/12 | note:a ]",
|
||||
"[ 19/12 → 5/3 | note:a ]",
|
||||
"[ 5/3 → 7/4 | note:a ]",
|
||||
"[ 7/4 → 11/6 | note:c ]",
|
||||
"[ 11/6 → 23/12 | note:c ]",
|
||||
"[ 23/12 → 2/1 | note:c ]",
|
||||
"[ 2/1 → 25/12 | note:a ]",
|
||||
"[ 25/12 → 13/6 | note:a ]",
|
||||
"[ 13/6 → 9/4 | note:a ]",
|
||||
"[ 9/4 → 7/3 | note:a ]",
|
||||
"[ 7/3 → 29/12 | note:a ]",
|
||||
"[ 29/12 → 5/2 | note:a ]",
|
||||
"[ 5/2 → 31/12 | note:c ]",
|
||||
"[ 31/12 → 8/3 | note:c ]",
|
||||
"[ 8/3 → 11/4 | note:c ]",
|
||||
"[ 11/4 → 17/6 | note:c ]",
|
||||
"[ 17/6 → 35/12 | note:c ]",
|
||||
"[ 35/12 → 3/1 | note:c ]",
|
||||
"[ 3/1 → 37/12 | note:c ]",
|
||||
"[ 37/12 → 19/6 | note:c ]",
|
||||
"[ 19/6 → 13/4 | note:c ]",
|
||||
"[ 13/4 → 10/3 | note:f ]",
|
||||
"[ 10/3 → 41/12 | note:f ]",
|
||||
"[ 41/12 → 7/2 | note:f ]",
|
||||
"[ 7/2 → 43/12 | note:a ]",
|
||||
"[ 43/12 → 11/3 | note:a ]",
|
||||
"[ 11/3 → 15/4 | note:a ]",
|
||||
"[ 15/4 → 23/6 | note:c ]",
|
||||
"[ 23/6 → 47/12 | note:c ]",
|
||||
"[ 47/12 → 4/1 | note:c ]",
|
||||
]
|
||||
`;
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ export function SoundsTab() {
|
||||
try {
|
||||
// Pre-load the sample by calling onTrigger with a future time
|
||||
// This triggers the loading but schedules playback for later
|
||||
const time = ctx.currentTime + 0.5; // Give 500ms for loading
|
||||
const time = ctx.currentTime + 0.05;
|
||||
const ref = await onTrigger(time, params, onended);
|
||||
trigRef.current = ref;
|
||||
if (ref?.node) {
|
||||
|
||||
Reference in New Issue
Block a user