Add LFOs and envelopes
This commit is contained in:
parent
8c3ee6db6b
commit
917d01c47d
5 changed files with 247 additions and 34 deletions
|
|
@ -98,6 +98,90 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
|||
*/
|
||||
export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition');
|
||||
|
||||
/**
|
||||
* Attack time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtPosAttack
|
||||
* @synonyms wtPosAtt
|
||||
* @param {number | Pattern} time attack time in seconds
|
||||
*/
|
||||
export const { wtPosAttack, wtPosAtt } = registerControl('wtPosAttack', 'wtPosAtt');
|
||||
|
||||
/**
|
||||
* Decay time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtPosDecay
|
||||
* @synonyms wtPosDec
|
||||
* @param {number | Pattern} time decay time in seconds
|
||||
*/
|
||||
export const { wtPosDecay, wtPosDec } = registerControl('wtPosDecay', 'wtPosDec');
|
||||
|
||||
/**
|
||||
* Sustain time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtPosAttack
|
||||
* @synonyms wtPosSus
|
||||
* @param {number | Pattern} gain sustain level (0 to 1)
|
||||
*/
|
||||
export const { wtPosSustain, wtPosSus } = registerControl('wtPosSustain', 'wtPosSus');
|
||||
|
||||
/**
|
||||
* Release time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtPosRelease
|
||||
* @synonyms wtPosRel
|
||||
* @param {number | Pattern} time release time in seconds
|
||||
*/
|
||||
export const { wtPosRelease, wtPosRel } = registerControl('wtPosRelease', 'wtPosRel');
|
||||
|
||||
/**
|
||||
* Rate of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtPosRate
|
||||
* @param {number | Pattern} rate rate in hertz
|
||||
*/
|
||||
export const { wtPosRate } = registerControl('wtPosRate');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtPosDepth
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
*/
|
||||
export const { wtPosDepth } = registerControl('wtPosDepth');
|
||||
|
||||
/**
|
||||
* Whether to sync the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtPosSynced
|
||||
* @param {number | Pattern} synced Whether to sync the lfo to CPM. > 0.5 will be synced
|
||||
*/
|
||||
export const { wtPosSynced } = registerControl('wtPosSynced');
|
||||
|
||||
/**
|
||||
* Shape of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtPosShape
|
||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||
*/
|
||||
export const { wtPosShape } = registerControl('wtPosShape');
|
||||
|
||||
/**
|
||||
* DC offset of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtPosDCOffset
|
||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||
*/
|
||||
export const { wtPosDCOffset } = registerControl('wtPosDCOffset');
|
||||
|
||||
/**
|
||||
* Skew of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtPosSkew
|
||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||
*/
|
||||
export const { wtPosSkew } = registerControl('wtPosSkew');
|
||||
|
||||
/**
|
||||
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
|
||||
*
|
||||
|
|
@ -108,10 +192,94 @@ export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetableP
|
|||
* s("basique").bank("wt_digital").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1")
|
||||
* .wtWarpMode("spin")
|
||||
*/
|
||||
export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp');
|
||||
export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp')
|
||||
|
||||
/**
|
||||
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator.
|
||||
* Attack time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name wtWarpAttack
|
||||
* @synonyms wtWarpAtt
|
||||
* @param {number | Pattern} time attack time in seconds
|
||||
*/
|
||||
export const { wtWarpAttack, wtWarpAtt } = registerControl('wtWarpAttack', 'wtWarpAtt');
|
||||
|
||||
/**
|
||||
* Decay time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name wtWarpDecay
|
||||
* @synonyms wtWarpDec
|
||||
* @param {number | Pattern} time decay time in seconds
|
||||
*/
|
||||
export const { wtWarpDecay, wtWarpDec } = registerControl('wtWarpDecay', 'wtWarpDec');
|
||||
|
||||
/**
|
||||
* Sustain time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name wtWarpAttack
|
||||
* @synonyms wtWarpSus
|
||||
* @param {number | Pattern} gain sustain level (0 to 1)
|
||||
*/
|
||||
export const { wtWarpSustain, wtWarpSus } = registerControl('wtWarpSustain', 'wtWarpSus');
|
||||
|
||||
/**
|
||||
* Release time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name wtWarpRelease
|
||||
* @synonyms wtWarpRel
|
||||
* @param {number | Pattern} time release time in seconds
|
||||
*/
|
||||
export const { wtWarpRelease, wtWarpRel } = registerControl('wtWarpRelease', 'wtWarpRel');
|
||||
|
||||
/**
|
||||
* Rate of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name wtWarpRate
|
||||
* @param {number | Pattern} rate rate in hertz
|
||||
*/
|
||||
export const { wtWarpRate } = registerControl('wtWarpRate');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name wtWarpDepth
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
*/
|
||||
export const { wtWarpDepth } = registerControl('wtWarpDepth');
|
||||
|
||||
/**
|
||||
* Whether to sync the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name wtWarpSynced
|
||||
* @param {number | Pattern} synced Whether to sync the lfo to CPM. > 0.5 will be synced
|
||||
*/
|
||||
export const { wtWarpSynced } = registerControl('wtWarpSynced');
|
||||
|
||||
/**
|
||||
* Shape of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name wtWarpShape
|
||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||
*/
|
||||
export const { wtWarpShape } = registerControl('wtWarpShape');
|
||||
|
||||
/**
|
||||
* DC offset of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name wtWarpDCOffset
|
||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||
*/
|
||||
export const { wtWarpDCOffset } = registerControl('wtWarpDCOffset');
|
||||
|
||||
/**
|
||||
* Skew of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name wtWarpSkew
|
||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||
*/
|
||||
export const { wtWarpSkew } = registerControl('wtWarpSkew');
|
||||
|
||||
/**
|
||||
* Type of warp (alteration of the waveform) to apply to the wavetable oscillator.
|
||||
*
|
||||
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
|
||||
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
|
||||
|
|
|
|||
|
|
@ -97,6 +97,36 @@ export const getParamADSR = (
|
|||
param[ramp](min, end + release);
|
||||
};
|
||||
|
||||
function getModulationShapeInput(val) {
|
||||
if (typeof val === 'number') {
|
||||
return val % 5;
|
||||
}
|
||||
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
const { shape = 0, ...props } = properties;
|
||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||
debugger;
|
||||
const lfoprops = {
|
||||
frequency: 1,
|
||||
depth,
|
||||
skew: 0.5,
|
||||
phaseoffset: 0,
|
||||
time: begin,
|
||||
begin,
|
||||
end,
|
||||
shape: getModulationShapeInput(shape),
|
||||
dcoffset,
|
||||
min: dcoffset * depth,
|
||||
max: dcoffset * depth + depth,
|
||||
curve: 1,
|
||||
...props,
|
||||
};
|
||||
|
||||
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||
}
|
||||
|
||||
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||
const options = {
|
||||
threshold: threshold ?? -3,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import './reverb.mjs';
|
|||
import './vowel.mjs';
|
||||
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, gainNode, getCompressor, getWorklet, effectSend } from './helpers.mjs';
|
||||
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
|
|
@ -29,13 +29,6 @@ export function setMultiChannelOrbits(bool) {
|
|||
multiChannelOrbits = bool == true;
|
||||
}
|
||||
|
||||
function getModulationShapeInput(val) {
|
||||
if (typeof val === 'number') {
|
||||
return val % 5;
|
||||
}
|
||||
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||
}
|
||||
|
||||
export const soundMap = map();
|
||||
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
|
|
@ -314,28 +307,6 @@ export function connectToDestination(input, channels) {
|
|||
controller.output.connectToDestination(input, channels);
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
const { shape = 0, ...props } = properties;
|
||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||
const lfoprops = {
|
||||
frequency: 1,
|
||||
depth,
|
||||
skew: 0.5,
|
||||
phaseoffset: 0,
|
||||
time: begin,
|
||||
begin,
|
||||
end,
|
||||
shape: getModulationShapeInput(shape),
|
||||
dcoffset,
|
||||
min: dcoffset * depth,
|
||||
max: dcoffset * depth + depth,
|
||||
curve: 1,
|
||||
...props,
|
||||
};
|
||||
|
||||
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||
}
|
||||
|
||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
const ac = getAudioContext();
|
||||
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
||||
|
|
@ -682,6 +653,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
tremolo = cps * tremolosync;
|
||||
}
|
||||
|
||||
if (value.wtPosSynced != null) {
|
||||
value.wtPosRate /= cps;
|
||||
}
|
||||
|
||||
if (value.wtWarpSynced != null) {
|
||||
value.wtWarpRate /= cps;
|
||||
}
|
||||
|
||||
if (tremolo !== undefined) {
|
||||
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
||||
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { clamp } from './util.mjs';
|
||||
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
|
||||
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
destroyAudioWorkletNode,
|
||||
gainNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
destroyAudioWorkletNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
|
|
@ -235,7 +236,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
|||
const { tableUrl, label } = getTableInfo(value, tables);
|
||||
const payload = await loadWavetableFrames(tableUrl, label, frameLen);
|
||||
const holdEnd = t + duration;
|
||||
const envEnd = holdEnd + release + 0.01;
|
||||
const endWithRelease = holdEnd + release;
|
||||
const envEnd = endWithRelease + 0.01;
|
||||
const source = getWorklet(
|
||||
ac,
|
||||
'wavetable-oscillator-processor',
|
||||
|
|
@ -258,6 +260,37 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
|||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||
return;
|
||||
}
|
||||
const posADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease];
|
||||
const warpADSRParams = [value.wtPosAttack, value.wtPosDecay, value.wtPosSustain, value.wtPosRelease];
|
||||
const wtParams = source.parameters;
|
||||
const positionParam = wtParams.get('position');
|
||||
const warpParam = wtParams.get('warp');
|
||||
if (posADSRParams.some((p) => p !== undefined)) {
|
||||
const [pAttack, pDecay, pSustain, pRelease] = getADSRValues(posADSRParams);
|
||||
getParamADSR(positionParam, pAttack, pDecay, pSustain, pRelease, 0, 1, t, holdEnd, 'linear');
|
||||
} else {
|
||||
const posLFO = getLfo(ac, t, endWithRelease, {
|
||||
frequency: value.wtPosRate,
|
||||
depth: value.wtPosDepth,
|
||||
shape: value.wtPosShape,
|
||||
skew: value.wtPosSkew,
|
||||
dcoffset: value.wtPosDCOffset ?? 0,
|
||||
});
|
||||
posLFO.connect(positionParam);
|
||||
}
|
||||
if (posADSRParams.some((p) => p !== undefined)) {
|
||||
const [wAttack, wDecay, wSustain, wRelease] = getADSRValues(warpADSRParams);
|
||||
getParamADSR(warpParam, wAttack, wDecay, wSustain, wRelease, 0, 1, t, holdEnd, 'linear');
|
||||
} else {
|
||||
const warpLFO = getLfo(ac, t, endWithRelease, {
|
||||
frequency: value.wtWarpRate,
|
||||
depth: value.wtWarpDepth,
|
||||
shape: value.wtWarpShape,
|
||||
skew: value.wtWarpSkew,
|
||||
dcoffset: value.wtWarpDCOffset ?? 0,
|
||||
});
|
||||
warpLFO.connect(warpParam);
|
||||
}
|
||||
const vibratoOscillator = getVibratoOscillator(source.detune, value, t);
|
||||
const envGain = ac.createGain();
|
||||
const node = source.connect(envGain);
|
||||
|
|
@ -271,6 +304,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
|||
destroyAudioWorkletNode(source);
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
warpLFO.disconnect();
|
||||
posLFO.disconnect();
|
||||
onended();
|
||||
},
|
||||
t,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue