diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 2565b73b..b5846596 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -93,22 +93,193 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); * @name wtPos * @param {number | Pattern} position Position in the wavetable from 0 to 1 * @synonyms wavetablePosition - * + * @example + * s("squelch").bank("wt_digital").seg(8).note("F1").wtPos("0 0.25 0.5 0.75 1") */ 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 * * @name wtWarp * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 * @synonyms wavetableWarp - * + * @example + * 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'); /** - * 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 @@ -116,6 +287,9 @@ export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWar * @name wtWarpMode * @param {number | string | Pattern} mode Warp mode * @synonyms wavetableWarpMode + * @example + * s("morgana").bank("wt_digital").seg(8).note("F1").wtWarp("0 0.25 0.5 0.75 1") + * .wtWarpMode("*2") * */ export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', 'wavetableWarpMode'); @@ -126,6 +300,8 @@ export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', ' * @name wtPhaseRand * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) * @synonyms wavetablePhaseRand + * @example + * s("basique").bank("wt_digital").seg(16).wtPhaseRand("<0 1>") * */ export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand'); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 4921d873..859329b2 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -97,6 +97,35 @@ 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; + 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, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 63ed7d30..48264ec0 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -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 diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 5b1b4edf..5dc5dc08 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -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, diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 0ba09517..475c05f6 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,6 +76,7 @@ export function cycleToSeconds(cycle, cps) { export function secondsToCycle(t, cps) { return t * cps; } + // deduces relevant info for sample loading from hap.value and sample definition // it encapsulates the core sampler logic into a pure and synchronous function // hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index c54f3a88..095bd079 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,9 +1,10 @@ import { getAudioContext, registerSound } from './index.mjs'; -import { getCommonSampleInfo, getSoundIndex, valueToMidi } from './util.mjs'; +import { getCommonSampleInfo } from './util.mjs'; import { destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, + getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, @@ -39,8 +40,7 @@ export const WarpMode = Object.freeze({ }); async function loadWavetableFrames(url, label, frameLen = 2048) { - const ac = getAudioContext(); - const buf = await loadBuffer(url, ac, label); + const buf = await loadBuffer(url, label); const ch0 = buf.getChannelData(0); const total = ch0.length; const numFrames = Math.max(1, Math.floor(total / frameLen)); @@ -86,11 +86,37 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export function getTableInfo(hapValue, urls) { - return getCommonSampleInfo(hapValue, urls); +// Extract the sample rate of a .wav file +function parseWavSampleRate(arrBuf) { + const dv = new DataView(arrBuf); + // Header is "RIFFWAVE", so 12 bytes + let p = 12; + // Look through chunks for the format header + // (they will always have an 8 byte header (id and size) followed by a payload) + while (p + 8 <= dv.byteLength) { + // Parse id + const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3)); + // Parse chunk size + const size = dv.getUint32(p + 4, true); + if (id === 'fmt ') { + // The format chunk contains the sample rate after + // 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels + // (for a total of 12) + return dv.getUint32(p + 12, true); + } + // Advance to next chunk + p += 8 + size + (size & 1); + } + return null; } -const loadBuffer = (url, ac, label) => { +async function decodeAtNativeRate(arr) { + const sr = parseWavSampleRate(arr) || 44100; + const tempAC = new OfflineAudioContext(1, 1, sr); + return await tempAC.decodeAudioData(arr); +} + +const loadBuffer = (url, label) => { url = url.replace('#', '%23'); if (!loadCache[url]) { logger(`[wavetable] load table ${label}..`, 'load-table', { url }); @@ -101,7 +127,7 @@ const loadBuffer = (url, ac, label) => { const took = Date.now() - timestamp; const size = humanFileSize(res.byteLength); logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); - const decoded = await ac.decodeAudioData(res); + const decoded = await decodeAtNativeRate(res); return decoded; }); } @@ -121,27 +147,39 @@ function githubPath(base, subpath = '') { return `https://raw.githubusercontent.com/${path}/${subpath}`; } -const _processTables = (json, baseUrl, frameLen) => { - return Object.entries(json).forEach(([key, value]) => { - if (typeof value === 'string') { - value = [value]; +const _processTables = (json, baseUrl, frameLen, options = {}) => { + baseUrl = json._base || baseUrl; + return Object.entries(json).forEach(([key, tables]) => { + if (key === '_base') return false; + if (typeof tables === 'string') { + tables = [tables]; } - if (typeof value !== 'object') { + if (typeof tables !== 'object') { throw new Error('wrong json format for ' + key); } - baseUrl = value._base || baseUrl; - if (baseUrl.startsWith('github:')) { - baseUrl = githubPath(baseUrl, ''); + let resolvedUrl = baseUrl; + if (resolvedUrl.startsWith('github:')) { + resolvedUrl = githubPath(resolvedUrl, ''); + } + tables = tables + .map((t) => resolvedUrl + t) + .filter((t) => { + if (!t.toLowerCase().endsWith('.wav')) { + logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`); + return false; + } + return true; + }); + if (tables.length) { + registerWaveTable(key, tables, { baseUrl, frameLen }); } - value = value.map((v) => baseUrl + v); - registerWaveTable(key, value, { baseUrl, frameLen }); }); }; -export function registerWaveTable(key, bank, params) { - registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, bank, params?.frameLen ?? 2048), { +export function registerWaveTable(key, tables, params) { + registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, tables, params?.frameLen ?? 2048), { type: 'wavetable', - tables: bank, + tables, ...params, }); } @@ -151,7 +189,7 @@ export function registerWaveTable(key, bank, params) { * * @name tables */ -export const tables = async (url, frameLen, json) => { +export const tables = async (url, frameLen, json, options = {}) => { if (json !== undefined) return _processTables(json, url, frameLen); if (url.startsWith('github:')) { url = githubPath(url, 'strudel.json'); @@ -169,14 +207,14 @@ export const tables = async (url, frameLen, json) => { } return fetch(url) .then((res) => res.json()) - .then((json) => _processTables(json, url, frameLen)) + .then((json) => _processTables(json, url, frameLen, options)) .catch((error) => { console.error(error); throw new Error(`error loading "${url}"`); }); }; -export async function onTriggerSynth(t, value, onended, bank, frameLen) { +export async function onTriggerSynth(t, value, onended, tables, frameLen) { const { s, n = 0, duration } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); @@ -185,10 +223,11 @@ export async function onTriggerSynth(t, value, onended, bank, frameLen) { wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; } const frequency = getFrequencyFromValue(value); - const { url, label } = getTableInfo(value, bank); + const { url, label } = getCommonSampleInfo(value, tables); const payload = await loadWavetableFrames(url, 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', @@ -211,6 +250,39 @@ export async function onTriggerSynth(t, value, onended, bank, 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'); + let posLFO; + 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); + } + let warpLFO; + 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); @@ -224,6 +296,8 @@ export async function onTriggerSynth(t, value, onended, bank, frameLen) { destroyAudioWorkletNode(source); vibratoOscillator?.stop(); node.disconnect(); + posLFO?.disconnect(); + warpLFO?.disconnect(); onended(); }, t, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 91eb6863..0a2d40d9 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1054,8 +1054,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warpMode', defaultValue: 0 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, - { name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 }, - { name: 'phaserand', defaultValue: 1, minValue: 0, maxValue: 1 }, + { name: 'spread', defaultValue: 0.18, minValue: 0, maxValue: 1 }, + { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, ]; } @@ -1222,10 +1222,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } _sampleFrame(frame, phase) { - const pos = phase * (frame.length - 1); + const pos = phase * frame.length; const i = pos | 0; const frac = pos - i; - const a = frame[i]; + const a = frame[i % frame.length]; const b = frame[(i + 1) % frame.length]; return a + (b - a) * frac; } @@ -1249,7 +1249,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); - const spread = pv(parameters.spread, i) * 0.5 + 0.5; const tablePos = pv(parameters.position, i); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; @@ -1257,11 +1256,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const warpAmount = pv(parameters.warp, i); const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); + const spread = voices > 1 ? pv(parameters.spread, i) : 0; const phaseRand = pv(parameters.phaserand, i); - const gain1 = Math.sqrt(1 - spread); - const gain2 = Math.sqrt(spread); + const gain1 = Math.sqrt(0.5 - 0.5 * spread); + 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 / voices; for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; let gainL = gain1; @@ -1274,19 +1275,19 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune const dPhase = fVoice / sampleRate; const level = this._chooseMip(dPhase); - const bank = this.tables[level]; + 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(bank[fIdx], ph); - const s1 = this._sampleFrame(bank[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; } - outL[i] += ((s * gainL) / Math.sqrt(voices)) * gainAdjustment; - outR[i] += ((s * gainR) / Math.sqrt(voices)) * gainAdjustment; + outL[i] += s * gainL * normalizer; + outR[i] += s * gainR * normalizer; this.phase[n] = wrapPhase(this.phase[n] + dPhase); } } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index a0597558..bfa4ae40 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -11993,6 +11993,234 @@ exports[`runs examples > example "withValue" example index 0 1`] = ` ] `; +exports[`runs examples > example "wtPhaseRand" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 1/16 → 1/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 1/8 → 3/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 3/16 → 1/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 1/4 → 5/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 5/16 → 3/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 3/8 → 7/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 7/16 → 1/2 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 1/2 → 9/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 9/16 → 5/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 5/8 → 11/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 11/16 → 3/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 3/4 → 13/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 13/16 → 7/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 7/8 → 15/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 15/16 → 1/1 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 1/1 → 17/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 17/16 → 9/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 9/8 → 19/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 19/16 → 5/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 5/4 → 21/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 21/16 → 11/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 11/8 → 23/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 23/16 → 3/2 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 3/2 → 25/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 25/16 → 13/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 13/8 → 27/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 27/16 → 7/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 7/4 → 29/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 29/16 → 15/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 15/8 → 31/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 31/16 → 2/1 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 2/1 → 33/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 33/16 → 17/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 17/8 → 35/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 35/16 → 9/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 9/4 → 37/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 37/16 → 19/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 19/8 → 39/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 39/16 → 5/2 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 5/2 → 41/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 41/16 → 21/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 21/8 → 43/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 43/16 → 11/4 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 11/4 → 45/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 45/16 → 23/8 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 23/8 → 47/16 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 47/16 → 3/1 | s:basique bank:wt_digital wtPhaseRand:0 ]", + "[ 3/1 → 49/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 49/16 → 25/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 25/8 → 51/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 51/16 → 13/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 13/4 → 53/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 53/16 → 27/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 27/8 → 55/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 55/16 → 7/2 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 7/2 → 57/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 57/16 → 29/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 29/8 → 59/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 59/16 → 15/4 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 15/4 → 61/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 61/16 → 31/8 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 31/8 → 63/16 | s:basique bank:wt_digital wtPhaseRand:1 ]", + "[ 63/16 → 4/1 | s:basique bank:wt_digital wtPhaseRand:1 ]", +] +`; + +exports[`runs examples > example "wtPos" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", + "[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", + "[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", + "[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", + "[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", + "[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", + "[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wtPos:0 ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wtPos:0.25 ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wtPos:0.5 ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wtPos:0.75 ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wtPos:1 ]", + "[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wtPos:1 ]", +] +`; + +exports[`runs examples > example "wtWarp" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:spin ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", + "[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 wtWarp:1 wtWarpMode:spin ]", +] +`; + +exports[`runs examples > example "wtWarpMode" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:asym ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:asym ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:asym ]", + "[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:asym ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:asym ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:asym ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:bendp ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:bendp ]", + "[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:bendp ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:bendp ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:bendp ]", + "[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:bendp ]", + "[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:logistic ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:logistic ]", + "[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:logistic ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:logistic ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:logistic ]", + "[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:logistic ]", + "[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:sync ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:sync ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:sync ]", + "[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:sync ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:sync ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:sync ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:wormhole ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", + "[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:wormhole ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:wormhole ]", + "[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:wormhole ]", + "[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:brownian ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 wtWarp:0 wtWarpMode:brownian ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:brownian ]", + "[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:brownian ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 wtWarp:0.25 wtWarpMode:brownian ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:brownian ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.5 wtWarpMode:asym ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:asym ]", + "[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:asym ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 wtWarp:0.75 wtWarpMode:asym ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:asym ]", + "[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 wtWarp:1 wtWarpMode:asym ]", +] +`; + exports[`runs examples > example "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json index be8a4e16..d9e0eeb9 100644 --- a/website/public/uzu-wavetables.json +++ b/website/public/uzu-wavetables.json @@ -18,5 +18,22 @@ "wt_digital/wt_squelch.wav", "wt_digital/wt_summer.wav", "wt_digital/wt_wasp.wav" - ] + ], + "wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"], + "wt_digital_basique": ["wt_digital/wt_basique.wav"], + "wt_digital_crickets": ["wt_digital/wt_crickets.wav"], + "wt_digital_curses": ["wt_digital/wt_curses.wav"], + "wt_digital_earl_grey": ["wt_digital/wt_earl_grey.wav"], + "wt_digital_echoes": ["wt_digital/wt_echoes.wav"], + "wt_digital_glimmer": ["wt_digital/wt_glimmer.wav"], + "wt_digital_majick": ["wt_digital/wt_majick.wav"], + "wt_digital_meditation": ["wt_digital/wt_meditation.wav"], + "wt_digital_morgana": ["wt_digital/wt_morgana.wav"], + "wt_digital_red_alert": ["wt_digital/wt_red_alert.wav"], + "wt_digital_sad_piano": ["wt_digital/wt_sad_piano.wav"], + "wt_digital_shook": ["wt_digital/wt_shook.wav"], + "wt_digital_sludge": ["wt_digital/wt_sludge.wav"], + "wt_digital_squelch": ["wt_digital/wt_squelch.wav"], + "wt_digital_summer": ["wt_digital/wt_summer.wav"], + "wt_digital_wasp": ["wt_digital/wt_wasp.wav"] } \ No newline at end of file diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 0484da02..363f6b98 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -44,6 +44,9 @@ export function SoundsTab() { if (soundsFilter === soundFilterType.SYNTHS) { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + if (soundsFilter === soundFilterType.WAVETABLES) { + return filtered.filter(([_, { data }]) => data.type === 'wavetable'); + } //TODO: tidy this up, it does not need to be saved in settings if (soundsFilter === 'importSounds') { return []; @@ -54,6 +57,9 @@ export function SoundsTab() { // holds mutable ref to current triggered sound const trigRef = useRef(); + // Used to cycle through sound previews on banks with multiple sounds + let soundPreviewIdx = 0; + // stop current sound on mouseup useEvent('mouseup', () => { const t = trigRef.current; @@ -74,6 +80,7 @@ export function SoundsTab() { samples: 'samples', drums: 'drum-machines', synths: 'Synths', + wavetables: 'Wavetables', user: 'User', importSounds: 'import-sounds', }} @@ -110,11 +117,13 @@ export function SoundsTab() { const params = { note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, s: name, + n: soundPreviewIdx, clip: 1, release: 0.5, sustain: 1, duration: 0.5, }; + soundPreviewIdx++; const time = ctx.currentTime + 0.05; const onended = () => trigRef.current?.node?.disconnect(); trigRef.current = Promise.resolve(onTrigger(time, params, onended)); @@ -126,6 +135,7 @@ export function SoundsTab() { {' '} {name} {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''} + {data?.type === 'wavetable' ? `(${getSamples(data.tables)})` : ''} {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''} ); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 9c3d7814..9365a6db 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -13,6 +13,7 @@ export const soundFilterType = { DRUMS: 'drums', SAMPLES: 'samples', SYNTHS: 'synths', + WAVETABLES: 'wavetables', ALL: 'all', };