mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a969c9a279 | |||
| e8db161126 | |||
| 2e4947bca6 | |||
| 96a6e0aeec |
@@ -1622,6 +1622,40 @@ export const { unison } = registerControl('unison');
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { spread } = registerControl('spread');
|
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.
|
* Set dryness of reverb. See `room` and `size` for more information about reverb.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp, midiToFreq, nanFallback, noteToMidi } from './util.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
@@ -525,3 +525,23 @@ export const destroyAudioWorkletNode = (node) => {
|
|||||||
node.disconnect();
|
node.disconnect();
|
||||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -174,22 +174,25 @@ export function registerSynthSounds() {
|
|||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
freqspread: detune,
|
freqspread: detune,
|
||||||
voices,
|
|
||||||
panspread,
|
panspread,
|
||||||
|
power: value.dpow,
|
||||||
|
blend: value.dblend,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outputChannelCount: [2],
|
outputChannelCount: [2],
|
||||||
|
processorOptions: {
|
||||||
|
voices,
|
||||||
|
stackmode: value.dstack,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3, begin, holdend, 'linear');
|
||||||
|
|
||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
|
|||||||
@@ -234,12 +234,19 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
freqspread: value.detune,
|
freqspread: value.detune,
|
||||||
position: value.wt,
|
position: value.wt,
|
||||||
warp: value.warp,
|
warp: value.warp,
|
||||||
warpMode: warpmode,
|
|
||||||
voices: Math.max(value.unison ?? 1, 1),
|
|
||||||
panspread: value.spread,
|
panspread: value.spread,
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ outputChannelCount: [2] },
|
|
||||||
);
|
);
|
||||||
source.port.postMessage({ type: 'table', payload });
|
source.port.postMessage({ type: 'table', payload });
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import OLAProcessor from './ola-processor';
|
import OLAProcessor from './ola-processor';
|
||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
import { getDistortionAlgorithm, getDetuner } from './helpers.mjs';
|
||||||
|
|
||||||
const blockSize = 128;
|
const blockSize = 128;
|
||||||
const PI = Math.PI;
|
const PI = Math.PI;
|
||||||
@@ -28,16 +28,6 @@ const fast_tanh = (x) => {
|
|||||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
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) => {
|
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
||||||
return frequency * Math.pow(2, detune / 12);
|
return frequency * Math.pow(2, detune / 12);
|
||||||
};
|
};
|
||||||
@@ -454,11 +444,68 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('distort-processor', DistortProcessor);
|
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
|
// SUPERSAW
|
||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor({ processorOptions }) {
|
||||||
super();
|
super();
|
||||||
this.phase = [];
|
initUnisonProcessor(this, processorOptions);
|
||||||
}
|
}
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
@@ -468,20 +515,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
max: Number.POSITIVE_INFINITY,
|
max: Number.POSITIVE_INFINITY,
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'end',
|
name: 'end',
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
max: Number.POSITIVE_INFINITY,
|
max: Number.POSITIVE_INFINITY,
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'frequency',
|
name: 'frequency',
|
||||||
defaultValue: 440,
|
defaultValue: 440,
|
||||||
min: Number.EPSILON,
|
min: Number.EPSILON,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'panspread',
|
name: 'panspread',
|
||||||
defaultValue: 0.4,
|
defaultValue: 0.4,
|
||||||
@@ -498,12 +542,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'voices',
|
name: 'blend',
|
||||||
defaultValue: 5,
|
defaultValue: 0,
|
||||||
min: 1,
|
min: -1,
|
||||||
automationRate: 'k-rate',
|
max: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'power',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: -1,
|
||||||
|
max: 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -512,39 +561,35 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (currentTime >= params.end[0]) {
|
if (currentTime >= params.end[0]) {
|
||||||
// this.port.postMessage({ type: 'onended' });
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const voices = params.voices[0]; // k-rate
|
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
const detune = pv(params.detune, i);
|
const { blendGains, normalizer, detuner, inv, freq } = getUnisonData(this, params, i);
|
||||||
const freqspread = pv(params.freqspread, i);
|
|
||||||
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
||||||
let gainL = Math.sqrt(1 - panspread);
|
let gainL = Math.sqrt(1 - panspread) * normalizer;
|
||||||
let gainR = Math.sqrt(panspread);
|
let gainR = Math.sqrt(panspread) * normalizer;
|
||||||
let freq = pv(params.frequency, i);
|
for (let n = 0; n < this.voices; n++) {
|
||||||
// Main detuning
|
|
||||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
|
||||||
const detuner = getDetuner(voices, freqspread);
|
|
||||||
for (let n = 0; n < voices; n++) {
|
|
||||||
// Individual voice detuning
|
// Individual voice detuning
|
||||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
const freqVoice = this.isDetuned ? applySemitoneDetuneToFrequency(freq, detuner(n * inv)) : freq; // voice detune
|
||||||
// We must wrap this here because it is passed into sawblep below which
|
const bG = blendGains[n];
|
||||||
// has domain [0, 1]
|
for (let idx = 0; idx < this.numIntervals; idx++) {
|
||||||
const dt = ffrac(freqVoice * INVSR);
|
const g = bG * this.intervalGains[idx];
|
||||||
this.phase[n] = this.phase[n] ?? Math.random();
|
const freqStack = freqVoice * this.intervals[idx];
|
||||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
// We must wrap this here because it is passed into sawblep below which
|
||||||
|
// has domain [0, 1]
|
||||||
output[0][i] += v * gainL;
|
const dt = ffrac(freqStack * INVSR);
|
||||||
output[1][i] += v * gainR;
|
const voiceNum = n * this.numIntervals + idx;
|
||||||
|
const v = waveshapes.sawblep(this.phases[voiceNum], dt);
|
||||||
let pn = this.phase[n] + dt;
|
output[0][i] += v * gainL * g;
|
||||||
if (pn >= 1.0) pn -= 1.0;
|
output[1][i] += v * gainR * g;
|
||||||
this.phase[n] = pn;
|
let pn = this.phases[voiceNum] + dt;
|
||||||
// invert right and left gain
|
if (pn >= 1.0) pn -= 1.0;
|
||||||
gainL = gainR;
|
this.phases[voiceNum] = pn;
|
||||||
gainR = gainL;
|
// invert right and left gain
|
||||||
|
gainL = gainR;
|
||||||
|
gainR = gainL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1124,24 +1169,24 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||||
{ name: 'warp', 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: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'blend', defaultValue: 0, min: -1, max: 1 },
|
||||||
|
{ name: 'power', defaultValue: 0, min: -1, max: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(options) {
|
constructor({ processorOptions }) {
|
||||||
super(options);
|
super();
|
||||||
|
initUnisonProcessor(this, processorOptions);
|
||||||
|
this.warpmode = processorOptions.warpmode;
|
||||||
this.frameLen = 0;
|
this.frameLen = 0;
|
||||||
this.numFrames = 0;
|
this.numFrames = 0;
|
||||||
this.phase = [];
|
|
||||||
|
|
||||||
this.port.onmessage = (e) => {
|
this.port.onmessage = (e) => {
|
||||||
const { type, payload } = e.data || {};
|
const { type, payload } = e.data || {};
|
||||||
if (type === 'table') {
|
if (type === 'table') {
|
||||||
const key = payload.key;
|
const key = payload.key;
|
||||||
this.frameLen = payload.frameLen;
|
this.frameLen = payload.frameLen;
|
||||||
|
this.baseThreshold = this.frameLen >>> 3; // used for mipmaps
|
||||||
if (!tablesCache[key]) {
|
if (!tablesCache[key]) {
|
||||||
const tables = [payload.frames];
|
const tables = [payload.frames];
|
||||||
let table = tables[0];
|
let table = tables[0];
|
||||||
@@ -1309,10 +1354,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_chooseMip(dphi) {
|
_chooseMip(dphi) {
|
||||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
||||||
let level = 0;
|
const numTables = this.tables.length;
|
||||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
let level = 0,
|
||||||
|
threshold = this.baseThreshold;
|
||||||
|
while (level + 1 < numTables && approxHarm < threshold) {
|
||||||
level++;
|
level++;
|
||||||
|
threshold *= 2;
|
||||||
}
|
}
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
@@ -1331,50 +1379,41 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
if (outR !== outL) outR.set(outL);
|
if (outR !== outL) outR.set(outL);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const voices = parameters.voices[0]; // k-rate
|
|
||||||
for (let i = 0; i < outL.length; i++) {
|
for (let i = 0; i < outL.length; i++) {
|
||||||
const detune = pv(parameters.detune, i);
|
const { blendGains, normalizer, detuner, inv, freq } = getUnisonData(this, parameters, i);
|
||||||
const freqspread = pv(parameters.freqspread, i);
|
|
||||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
const fIdx = idx | 0;
|
const fIdx = idx | 0;
|
||||||
const frac = idx - fIdx;
|
const frac = idx - fIdx;
|
||||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||||
const warpMode = pv(parameters.warpMode, i);
|
const panspread = this.isDetuned ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
let gainL = Math.sqrt(0.5 - 0.5 * panspread) * normalizer;
|
||||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
let gainR = Math.sqrt(0.5 + 0.5 * panspread) * normalizer;
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
for (let n = 0; n < this.voices; n++) {
|
||||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
const fVoice = this.isDetuned ? applySemitoneDetuneToFrequency(freq, detuner(n * inv)) : freq; // voice detune
|
||||||
let f = pv(parameters.frequency, i);
|
const bG = blendGains[n];
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
for (let idx = 0; idx < this.numIntervals; idx++) {
|
||||||
const normalizer = 1 / Math.sqrt(voices);
|
const g = bG * this.intervalGains[idx];
|
||||||
const detuner = getDetuner(voices, freqspread);
|
const fStack = fVoice * this.intervals[idx];
|
||||||
for (let n = 0; n < voices; n++) {
|
const dt = fStack * INVSR;
|
||||||
const isOdd = (n & 1) == 1;
|
const voiceNum = n * this.numIntervals + idx;
|
||||||
let gainL = gain1;
|
const level = this._chooseMip(dt);
|
||||||
let gainR = gain2;
|
const table = this.tables[level];
|
||||||
// invert right and left gain
|
// warp phase then sample
|
||||||
if (isOdd) {
|
const ph = this._warpPhase(this.phases[voiceNum], warpAmount, this.warpmode);
|
||||||
gainL = gain2;
|
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||||
gainR = gain1;
|
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 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 = s0 + (s1 - s0) * frac;
|
|
||||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
|
||||||
s = -s;
|
|
||||||
}
|
|
||||||
outL[i] += s * gainL * normalizer;
|
|
||||||
outR[i] += s * gainR * normalizer;
|
|
||||||
this.phase[n] = ffrac(this.phase[n] + dPhase);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user