diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 505e6ac38..44367f786 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -38,21 +38,25 @@ export const Warpmode = Object.freeze({ FLIP: 21, }); -async function loadWavetableFrames(url, label, frameLen = 2048) { - const buf = await loadBuffer(url, label); - const ch0 = buf.getChannelData(0); - const total = ch0.length; - const numFrames = Math.max(1, Math.floor(total / frameLen)); - const frames = new Array(numFrames); - for (let i = 0; i < numFrames; i++) { - const start = i * frameLen; - frames[i] = ch0.subarray(start, start + frameLen); +const seenKeys = new Set(); +async function getPayload(url, label, frameLen = 2048) { + const key = `${url},${frameLen}`; + if (!seenKeys.has(key)) { + const buf = await loadBuffer(url, label); + const ch0 = buf.getChannelData(0); + const total = ch0.length; + const numFrames = Math.max(1, Math.floor(total / frameLen)); + const frames = new Array(numFrames); + for (let i = 0; i < numFrames; i++) { + const start = i * frameLen; + frames[i] = ch0.subarray(start, start + frameLen); + } + seenKeys.add(key); + return { frames, frameLen, numFrames, key }; } - return { frames, frameLen, numFrames }; + return { frameLen, key }; // worklet will use the cached version } -const loadCache = {}; - function humanFileSize(bytes, si) { var thresh = si ? 1000 : 1024; if (bytes < thresh) return bytes + ' B'; @@ -97,6 +101,7 @@ async function decodeAtNativeRate(arr) { return await tempAC.decodeAudioData(arr); } +const loadCache = {}; const loadBuffer = (url, label) => { url = url.replace('#', '%23'); if (!loadCache[url]) { @@ -211,7 +216,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { } const frequency = getFrequencyFromValue(value); const { url, label } = getCommonSampleInfo(value, tables); - const payload = await loadWavetableFrames(url, label, frameLen); + const payload = await getPayload(url, label, frameLen); let holdEnd = t + duration; if (clip !== undefined) { holdEnd = Math.min(t + clip * duration, holdEnd); @@ -305,7 +310,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); - getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); const handle = { node, source }; const timeoutNode = webAudioTimeout( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index aa49ea32a..9b190a456 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1043,6 +1043,7 @@ function brownian(x, oct = 4) { return (sum / norm) * 2 - 1; } +const tablesCache = {}; class WavetableOscillatorProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ @@ -1061,7 +1062,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { constructor(options) { super(options); - this.frames = null; this.frameLen = 0; this.numFrames = 0; this.phase = []; @@ -1069,9 +1069,28 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.port.onmessage = (e) => { const { type, payload } = e.data || {}; if (type === 'table') { - this.frames = payload.frames; + const key = payload.key; this.frameLen = payload.frameLen; - this.numFrames = this.frames.length; + if (!tablesCache[key]) { + const tables = [payload.frames]; + let table = tables[0]; + for (let level = 1; level < 1; level++) { + const nextLen = table.length >> 1; + const nextTable = table.map((frame) => { + const avg = new Float32Array(nextLen); + for (let i = 0; i < nextLen; i++) { + avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2; + } + return avg; + }); + tables.push(nextTable); + table = nextTable; + if (nextLen <= 32) break; + } + tablesCache[key] = tables; + } + this.tables = tablesCache[key]; + this.numFrames = this.tables[0].length; } }; } @@ -1222,6 +1241,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { return a + (b - a) * frac; } + _chooseMip(dphi) { + 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++; + } + return level; + } + process(_inputs, outputs, parameters) { if (currentTime >= parameters.end[0]) { return false; @@ -1231,8 +1259,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - - if (!this.frames) { + if (!this.tables) { outL.fill(0); if (outR !== outL) outR.set(outL); return true; @@ -1253,7 +1280,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const gain2 = Math.sqrt(0.5 + 0.5 * spread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune - const normalizer = 0.3 / Math.sqrt(voices); + const normalizer = 1 / Math.sqrt(voices); for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; let gainL = gain1; @@ -1265,12 +1292,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, 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(this.frames[fIdx], ph); - const s1 = this._sampleFrame(this.frames[Math.min(this.numFrames - 1, fIdx + 1)], ph); + 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;