Compare commits

...

17 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 48718daea8 add_vgame 2025-09-28 19:57:19 -04:00
Jade (Rose) Rowland 8d45016ccf working 2025-09-28 19:11:13 -04:00
Switch Angel AKA Jade Rose ec69f6fd77 Merge pull request 'rename wavetable controls to match existing naming conventions' (#1610) from wavetable_controls into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1610
2025-09-28 21:28:15 +02:00
Jade (Rose) Rowland d94f145649 fix vib 2025-09-28 15:23:58 -04:00
Jade (Rose) Rowland bf3fe605d9 fix unison gain 2025-09-28 15:21:06 -04:00
Jade (Rose) Rowland 54ebc97ddc fix tests 2025-09-28 15:07:35 -04:00
Jade (Rose) Rowland c689874bb4 simplify modulators 2025-09-28 14:57:12 -04:00
Jade (Rose) Rowland 23d4bfa9d9 rename controls 2025-09-28 09:19:39 -04:00
Switch Angel AKA Jade Rose 91eb50f1c1 Merge pull request '<Warn: Don't merge until tables uploaded> Feature: Wavetable synth improvements' (#1607) from glossing/strudel:glossing/wavetable-synth into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1607
2025-09-27 23:28:12 +02:00
Aria 5838ade740 Merge branch 'main' into glossing/wavetable-synth 2025-09-27 13:17:05 -07:00
Aria 917d01c47d Add LFOs and envelopes 2025-09-27 12:48:37 -07:00
Switch Angel AKA Jade Rose 0a51718df3 Merge pull request 'wavetable improvements' (#1608) from wt_improvements into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1608
2025-09-27 21:43:09 +02:00
Aria 8c3ee6db6b Missed deletion 2025-09-27 11:07:04 -07:00
Aria c5c05ebadc Clean up to align with other PR 2025-09-27 11:02:57 -07:00
Aria a8918d55fb Actually update normalizer 2025-09-27 10:45:30 -07:00
Aria 466234bad6 Merge branch 'main' into glossing/wavetable-synth 2025-09-26 21:22:45 -07:00
Aria 97beaec25a Added examples, fixed samplerate issue on import, added to settings tab, fixed spread, added default wavetables, change default phaserand to 0 2025-09-26 20:46:10 -07:00
11 changed files with 698 additions and 108 deletions
+201 -11
View File
@@ -90,45 +90,235 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
/** /**
* Position in the wavetable of the wavetable oscillator * Position in the wavetable of the wavetable oscillator
* *
* @name wtPos * @name wt
* @param {number | Pattern} position Position in the wavetable from 0 to 1 * @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition * @synonyms wavetablePosition
* * @example
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
*/ */
export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition'); export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name wtenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { wtenv } = registerControl('wtenv');
/**
* Attack time of the wavetable oscillator's position envelope
*
* @name wtattack
* @synonyms wtatt
* @param {number | Pattern} time attack time in seconds
*/
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
/**
* Decay time of the wavetable oscillator's position envelope
*
* @name wtdecay
* @synonyms wtdec
* @param {number | Pattern} time decay time in seconds
*/
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
/**
* Sustain time of the wavetable oscillator's position envelope
*
* @name wtsustain
* @synonyms wtsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
/**
* Release time of the wavetable oscillator's position envelope
*
* @name wtrelease
* @synonyms wtrel
* @param {number | Pattern} time release time in seconds
*/
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
/**
* Rate of the LFO for the wavetable oscillator's position
*
* @name wtrate
* @param {number | Pattern} rate rate in hertz
*/
export const { wtrate } = registerControl('wtrate');
/**
* cycle synced rate of the LFO for the wavetable oscillator's position
*
* @name wtsync
* @param {number | Pattern} rate rate in cycles
*/
export const { wtsync } = registerControl('wtsync');
/**
* Depth of the LFO for the wavetable oscillator's position
*
* @name wtdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { wtdepth } = registerControl('wtdepth');
/**
* Shape of the LFO for the wavetable oscillator's position
*
* @name wtshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { wtshape } = registerControl('wtshape');
/**
* DC offset of the LFO for the wavetable oscillator's position
*
* @name wtdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { wtdc } = registerControl('wtdc');
/**
* Skew of the LFO for the wavetable oscillator's position
*
* @name wtskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { wtskew } = registerControl('wtskew');
/** /**
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
* *
* @name wtWarp * @name warp
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1 * @param {number | Pattern} amount Warp of the wavetable from 0 to 1
* @synonyms wavetableWarp * @synonyms wavetableWarp
* * @example
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("spin")
*/ */
export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp'); export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
/** /**
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator. * Attack time of the wavetable oscillator's warp envelope
*
* @name warpattack
* @synonyms warpatt
* @param {number | Pattern} time attack time in seconds
*/
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
/**
* Decay time of the wavetable oscillator's warp envelope
*
* @name warpdecay
* @synonyms warpdec
* @param {number | Pattern} time decay time in seconds
*/
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
/**
* Sustain time of the wavetable oscillator's warp envelope
*
* @name warpsustain
* @synonyms warpsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
/**
* Release time of the wavetable oscillator's warp envelope
*
* @name warprelease
* @synonyms warprel
* @param {number | Pattern} time release time in seconds
*/
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
/**
* Rate of the LFO for the wavetable oscillator's warp
*
* @name warprate
* @param {number | Pattern} rate rate in hertz
*/
export const { warprate } = registerControl('warprate');
/**
* Depth of the LFO for the wavetable oscillator's warp
*
* @name warpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { warpdepth } = registerControl('warpdepth');
/**
* Shape of the LFO for the wavetable oscillator's warp
*
* @name warpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { warpshape } = registerControl('warpshape');
/**
* DC offset of the LFO for the wavetable oscillator's warp
*
* @name warpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { warpdc } = registerControl('warpdc');
/**
* Skew of the LFO for the wavetable oscillator's warp
*
* @name warpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { warpskew } = registerControl('warpskew');
/**
* 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, * 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 * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
* *
* @name wtWarpMode * @name warpmode
* @param {number | string | Pattern} mode Warp mode * @param {number | string | Pattern} mode Warp mode
* @synonyms wavetableWarpMode * @synonyms wavetableWarpMode
* @example
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
* *
*/ */
export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', 'wavetableWarpMode'); export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode');
/** /**
* Amount of randomness of the initial phase of the wavetable oscillator. * Amount of randomness of the initial phase of the wavetable oscillator.
* *
* @name wtPhaseRand * @name wtphaserand
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
* @synonyms wavetablePhaseRand * @synonyms wavetablePhaseRand
* @example
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
* *
*/ */
export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand'); export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name warpenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { warpenv } = registerControl('warpenv');
/**
* cycle synced rate of the LFO for the wavetable warp position
*
* @name warpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { warpsync } = registerControl('warpsync');
/** /**
* Define a custom webaudio node to use as a sound source. * Define a custom webaudio node to use as a sound source.
+64
View File
@@ -97,6 +97,35 @@ export const getParamADSR = (
param[ramp](min, end + release); 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) { export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const options = { const options = {
threshold: threshold ?? -3, threshold: threshold ?? -3,
@@ -124,6 +153,41 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
}; };
// helper utility for applying standard modulators to a parameter
export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) {
let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues;
if (amount == null) {
const hasADSRParams = values.some((p) => p != null);
amount = hasADSRParams ? defaultAmount : 0;
}
const min = offset ?? 0;
const max = amount + min;
const diff = Math.abs(max - min);
if (diff) {
const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues);
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
let lfo;
let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues;
if (depth == null) {
const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null);
depth = hasLFOParams ? defaultDepth : 0;
}
if (depth) {
lfo = getLfo(audioContext, start, end, {
depth,
dcoffset,
...getLfoInputs,
});
lfo.connect(param);
}
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) { export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
const curve = 'exponential'; const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]); const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
+10 -31
View File
@@ -9,7 +9,7 @@ import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet'; 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 { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs'; import { loadBuffer } from './sampler.mjs';
@@ -29,13 +29,6 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true; 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 const soundMap = map();
export function registerSound(key, onTrigger, data = {}) { export function registerSound(key, onTrigger, data = {}) {
@@ -314,28 +307,6 @@ export function connectToDestination(input, channels) {
controller.output.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) { function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext(); const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
@@ -584,7 +555,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioNodes.forEach((n) => n?.disconnect()); audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID); activeSoundSources.delete(chainID);
}; };
const soundHandle = await onTrigger(t, value, onEnded); const soundHandle = await onTrigger(t, value, onEnded, cps);
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
@@ -682,6 +653,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
tremolo = cps * tremolosync; tremolo = cps * tremolosync;
} }
if (value.wtPosSynced != null) {
value.wtPosRate /= cps;
}
if (value.wtWarpSynced != null) {
value.wtWarpRate /= cps;
}
if (tremolo !== undefined) { if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload // 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 // EX: a triangle waveform will clip like this /-\ when the depth is above 1
+2 -1
View File
@@ -1,11 +1,12 @@
import { clamp } from './util.mjs'; import { clamp } from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs'; import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
import { import {
applyFM, applyFM,
destroyAudioWorkletNode, destroyAudioWorkletNode,
gainNode, gainNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
getLfo,
getParamADSR, getParamADSR,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
+1
View File
@@ -76,6 +76,7 @@ export function cycleToSeconds(cycle, cps) {
export function secondsToCycle(t, cps) { export function secondsToCycle(t, cps) {
return t * cps; return t * cps;
} }
// deduces relevant info for sample loading from hap.value and sample definition // deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function // 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) // hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
+147 -37
View File
@@ -1,9 +1,11 @@
import { getAudioContext, registerSound } from './index.mjs'; import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfo, getSoundIndex, valueToMidi } from './util.mjs'; import { getCommonSampleInfo } from './util.mjs';
import { import {
applyParameterModulators,
destroyAudioWorkletNode, destroyAudioWorkletNode,
getADSRValues, getADSRValues,
getFrequencyFromValue, getFrequencyFromValue,
getLfo,
getParamADSR, getParamADSR,
getPitchEnvelope, getPitchEnvelope,
getVibratoOscillator, getVibratoOscillator,
@@ -13,7 +15,7 @@ import {
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
const WT_MAX_MIP_LEVELS = 6; const WT_MAX_MIP_LEVELS = 6;
export const WarpMode = Object.freeze({ export const Warpmode = Object.freeze({
NONE: 0, NONE: 0,
ASYM: 1, ASYM: 1,
MIRROR: 2, MIRROR: 2,
@@ -39,8 +41,7 @@ export const WarpMode = Object.freeze({
}); });
async function loadWavetableFrames(url, label, frameLen = 2048) { async function loadWavetableFrames(url, label, frameLen = 2048) {
const ac = getAudioContext(); const buf = await loadBuffer(url, label);
const buf = await loadBuffer(url, ac, label);
const ch0 = buf.getChannelData(0); const ch0 = buf.getChannelData(0);
const total = ch0.length; const total = ch0.length;
const numFrames = Math.max(1, Math.floor(total / frameLen)); const numFrames = Math.max(1, Math.floor(total / frameLen));
@@ -86,11 +87,37 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u]; return bytes.toFixed(1) + ' ' + units[u];
} }
export function getTableInfo(hapValue, urls) { // Extract the sample rate of a .wav file
return getCommonSampleInfo(hapValue, urls); function parseWavSampleRate(arrBuf) {
const dv = new DataView(arrBuf);
// Header is "RIFF<chunk size (4 bytes)>WAVE", 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'); url = url.replace('#', '%23');
if (!loadCache[url]) { if (!loadCache[url]) {
logger(`[wavetable] load table ${label}..`, 'load-table', { url }); logger(`[wavetable] load table ${label}..`, 'load-table', { url });
@@ -101,7 +128,7 @@ const loadBuffer = (url, ac, label) => {
const took = Date.now() - timestamp; const took = Date.now() - timestamp;
const size = humanFileSize(res.byteLength); const size = humanFileSize(res.byteLength);
logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); 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; return decoded;
}); });
} }
@@ -121,29 +148,47 @@ function githubPath(base, subpath = '') {
return `https://raw.githubusercontent.com/${path}/${subpath}`; return `https://raw.githubusercontent.com/${path}/${subpath}`;
} }
const _processTables = (json, baseUrl, frameLen) => { const _processTables = (json, baseUrl, frameLen, options = {}) => {
return Object.entries(json).forEach(([key, value]) => { baseUrl = json._base || baseUrl;
if (typeof value === 'string') { return Object.entries(json).forEach(([key, tables]) => {
value = [value]; 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); throw new Error('wrong json format for ' + key);
} }
baseUrl = value._base || baseUrl; let resolvedUrl = baseUrl;
if (baseUrl.startsWith('github:')) { if (resolvedUrl.startsWith('github:')) {
baseUrl = githubPath(baseUrl, ''); 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) { export function registerWaveTable(key, tables, params) {
registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, bank, params?.frameLen ?? 2048), { registerSound(
key,
(t, hapValue, onended, cps) => {
return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048);
},
{
type: 'wavetable', type: 'wavetable',
tables: bank, tables,
...params, ...params,
}); },
);
} }
/** /**
@@ -151,7 +196,7 @@ export function registerWaveTable(key, bank, params) {
* *
* @name tables * @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 (json !== undefined) return _processTables(json, url, frameLen);
if (url.startsWith('github:')) { if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json'); url = githubPath(url, 'strudel.json');
@@ -169,26 +214,27 @@ export const tables = async (url, frameLen, json) => {
} }
return fetch(url) return fetch(url)
.then((res) => res.json()) .then((res) => res.json())
.then((json) => _processTables(json, url, frameLen)) .then((json) => _processTables(json, url, frameLen, options))
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
throw new Error(`error loading "${url}"`); throw new Error(`error loading "${url}"`);
}); });
}; };
export async function onTriggerSynth(t, value, onended, bank, frameLen) { export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const { s, n = 0, duration } = value; const { s, n = 0, duration } = value;
const ac = getAudioContext(); const ac = getAudioContext();
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
let { wtWarpMode } = value; let { warpmode } = value;
if (typeof wtWarpMode === 'string') { if (typeof warpmode === 'string') {
wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE; warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
} }
const frequency = getFrequencyFromValue(value); const frequency = getFrequencyFromValue(value);
const { url, label } = getTableInfo(value, bank); const { url, label } = getCommonSampleInfo(value, tables);
const payload = await loadWavetableFrames(url, label, frameLen); const payload = await loadWavetableFrames(url, label, frameLen);
const holdEnd = t + duration; const holdEnd = t + duration;
const envEnd = holdEnd + release + 0.01; const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet( const source = getWorklet(
ac, ac,
'wavetable-oscillator-processor', 'wavetable-oscillator-processor',
@@ -197,12 +243,12 @@ export async function onTriggerSynth(t, value, onended, bank, frameLen) {
end: envEnd, end: envEnd,
frequency, frequency,
detune: value.detune, detune: value.detune,
position: value.wtPos, position: value.wt,
warp: value.wtWarp, warp: value.warp,
warpMode: wtWarpMode, warpMode: warpmode,
voices: value.unison, voices: Math.max(value.unison ?? 1, 1),
spread: value.spread, spread: value.spread,
phaserand: value.wtPhaseRand, phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
}, },
{ outputChannelCount: [2] }, { outputChannelCount: [2] },
); );
@@ -211,11 +257,73 @@ export async function onTriggerSynth(t, value, onended, bank, frameLen) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return; return;
} }
const vibratoOscillator = getVibratoOscillator(source.detune, value, t); const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease];
const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease];
const wtParams = source.parameters;
const positionParam = wtParams.get('position');
const warpParam = wtParams.get('warp');
let wtrate = value.wtrate;
if (value.wtsync != null) {
wtrate = cps * value.wtsync;
}
const wtPosModulators = applyParameterModulators(
ac,
positionParam,
t,
endWithRelease,
{
offset: value.wt,
amount: value.wtenv,
defaultAmount: 0.5,
shape: 'linear',
values: posADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: wtrate,
depth: value.wtdepth,
defaultDepth: 0.5,
shape: value.wtshape,
skew: value.wtskew,
dcoffset: value.wtdc ?? 0,
},
);
let warprate = value.warprate;
if (value.warpsync != null) {
warprate = warprate = cps * value.warpsync;
}
const wtWarpModulators = applyParameterModulators(
ac,
warpParam,
t,
endWithRelease,
{
offset: value.warp,
amount: value.warpenv,
defaultAmount: 0.5,
shape: 'linear',
values: warpADSRParams,
holdEnd,
defaultValues: [0, 0.5, 0, 0.1],
},
{
frequency: warprate,
depth: value.warpdepth,
defaultDepth: 0.5,
shape: value.warpshape,
skew: value.warpskew,
dcoffset: value.warpdc ?? 0,
},
);
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
const envGain = ac.createGain(); const envGain = ac.createGain();
const node = source.connect(envGain); 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, 1, t, holdEnd, 'linear');
getPitchEnvelope(source.detune, value, t, holdEnd); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
const handle = { node, source }; const handle = { node, source };
const timeoutNode = webAudioTimeout( const timeoutNode = webAudioTimeout(
ac, ac,
@@ -224,6 +332,8 @@ export async function onTriggerSynth(t, value, onended, bank, frameLen) {
destroyAudioWorkletNode(source); destroyAudioWorkletNode(source);
vibratoOscillator?.stop(); vibratoOscillator?.stop();
node.disconnect(); node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended(); onended();
}, },
t, t,
+13 -13
View File
@@ -1054,8 +1054,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warpMode', defaultValue: 0 }, { name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
{ name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 }, { name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
{ name: 'phaserand', defaultValue: 1, minValue: 0, maxValue: 1 }, { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
]; ];
} }
@@ -1222,10 +1222,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
_sampleFrame(frame, phase) { _sampleFrame(frame, phase) {
const pos = phase * (frame.length - 1); const pos = phase * frame.length;
const i = pos | 0; const i = pos | 0;
const frac = pos - i; const frac = pos - i;
const a = frame[i]; const a = frame[i % frame.length];
const b = frame[(i + 1) % frame.length]; const b = frame[(i + 1) % frame.length];
return a + (b - a) * frac; return a + (b - a) * frac;
} }
@@ -1239,7 +1239,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
const outL = outputs[0][0]; const outL = outputs[0][0];
const outR = outputs[0][1] || outputs[0][0]; const outR = outputs[0][1] || outputs[0][0];
const gainAdjustment = 0.3;
if (!this.tables) { if (!this.tables) {
outL.fill(0); outL.fill(0);
@@ -1249,7 +1248,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
for (let i = 0; i < outL.length; i++) { for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i); const detune = pv(parameters.detune, i);
const spread = pv(parameters.spread, i) * 0.5 + 0.5;
const tablePos = pv(parameters.position, i); const tablePos = pv(parameters.position, i);
const idx = tablePos * (this.numFrames - 1); const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0; const fIdx = idx | 0;
@@ -1258,10 +1256,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const warpMode = pv(parameters.warpMode, i); const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i); const voices = pv(parameters.voices, i);
const phaseRand = pv(parameters.phaserand, i); const phaseRand = pv(parameters.phaserand, i);
const gain1 = Math.sqrt(1 - spread); const spread = voices > 1 ? pv(parameters.spread, i) : 0;
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); let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 0.3 / Math.sqrt(voices);
for (let n = 0; n < voices; n++) { for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1; const isOdd = (n & 1) == 1;
let gainL = gain1; let gainL = gain1;
@@ -1274,19 +1274,19 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const dPhase = fVoice / sampleRate; const dPhase = fVoice / sampleRate;
const level = this._chooseMip(dPhase); const level = this._chooseMip(dPhase);
const bank = this.tables[level]; const table = this.tables[level];
// warp phase then sample // warp phase then sample
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s0 = this._sampleFrame(bank[fIdx], ph); const s0 = this._sampleFrame(table[fIdx], ph);
const s1 = this._sampleFrame(bank[Math.min(this.numFrames - 1, fIdx + 1)], ph); const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
let s = s0 + (s1 - s0) * frac; let s = s0 + (s1 - s0) * frac;
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
s = -s; s = -s;
} }
outL[i] += ((s * gainL) / Math.sqrt(voices)) * gainAdjustment; outL[i] += s * gainL * normalizer;
outR[i] += ((s * gainR) / Math.sqrt(voices)) * gainAdjustment; outR[i] += s * gainR * normalizer;
this.phase[n] = wrapPhase(this.phase[n] + dPhase); this.phase[n] = wrapPhase(this.phase[n] + dPhase);
} }
} }
+228
View File
@@ -11789,6 +11789,112 @@ exports[`runs examples > example "vowel" example index 1 1`] = `
] ]
`; `;
exports[`runs examples > example "warp" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
"[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]",
]
`;
exports[`runs examples > example "warpmode" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:bendp ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]",
"[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:spin ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:logistic ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]",
"[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:sync ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:wormhole ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]",
"[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:brownian ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
"[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]",
]
`;
exports[`runs examples > example "wchoose" example index 0 1`] = ` exports[`runs examples > example "wchoose" example index 0 1`] = `
[ [
"[ 0/1 → 1/5 | note:c2 s:sine ]", "[ 0/1 → 1/5 | note:c2 s:sine ]",
@@ -11993,6 +12099,128 @@ exports[`runs examples > example "withValue" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "wt" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wt:0 ]",
"[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]",
"[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]",
"[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]",
"[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wt:1 ]",
"[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wt: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 "xfade" example index 0 1`] = ` exports[`runs examples > example "xfade" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:hh gain:0 ]", "[ 0/1 → 1/8 | s:hh gain:0 ]",
+19 -13
View File
@@ -5,18 +5,24 @@
"wt_digital/wt_basique.wav", "wt_digital/wt_basique.wav",
"wt_digital/wt_crickets.wav", "wt_digital/wt_crickets.wav",
"wt_digital/wt_curses.wav", "wt_digital/wt_curses.wav",
"wt_digital/wt_earl_grey.wav", "wt_digital/wt_echoes.wav"
"wt_digital/wt_echoes.wav", ],
"wt_digital/wt_glimmer.wav", "wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"],
"wt_digital/wt_majick.wav", "wt_digital_basique": ["wt_digital/wt_basique.wav"],
"wt_digital/wt_meditation.wav", "wt_digital_crickets": ["wt_digital/wt_crickets.wav"],
"wt_digital/wt_morgana.wav", "wt_digital_curses": ["wt_digital/wt_curses.wav"],
"wt_digital/wt_red_alert.wav", "wt_digital_echoes": ["wt_digital/wt_echoes.wav"],
"wt_digital/wt_sad_piano.wav", "wt_vgame": [
"wt_digital/wt_shook.wav", "wt_vgame/wt_vgame10.wav",
"wt_digital/wt_sludge.wav", "wt_vgame/wt_vgame11.wav",
"wt_digital/wt_squelch.wav", "wt_vgame/wt_vgame12.wav",
"wt_digital/wt_summer.wav", "wt_vgame/wt_vgame13.wav",
"wt_digital/wt_wasp.wav" "wt_vgame/wt_vgame14.wav",
"wt_vgame/wt_vgame15.wav",
"wt_vgame/wt_vgame16.wav",
"wt_vgame/wt_vgame17.wav",
"wt_vgame/wt_vgame18.wav",
"wt_vgame/wt_vgame19.wav",
"wt_vgame/wt_vgame20.wav"
] ]
} }
@@ -44,6 +44,9 @@ export function SoundsTab() {
if (soundsFilter === soundFilterType.SYNTHS) { if (soundsFilter === soundFilterType.SYNTHS) {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); 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 //TODO: tidy this up, it does not need to be saved in settings
if (soundsFilter === 'importSounds') { if (soundsFilter === 'importSounds') {
return []; return [];
@@ -54,6 +57,9 @@ export function SoundsTab() {
// holds mutable ref to current triggered sound // holds mutable ref to current triggered sound
const trigRef = useRef(); const trigRef = useRef();
// Used to cycle through sound previews on banks with multiple sounds
let soundPreviewIdx = 0;
// stop current sound on mouseup // stop current sound on mouseup
useEvent('mouseup', () => { useEvent('mouseup', () => {
const t = trigRef.current; const t = trigRef.current;
@@ -74,6 +80,7 @@ export function SoundsTab() {
samples: 'samples', samples: 'samples',
drums: 'drum-machines', drums: 'drum-machines',
synths: 'Synths', synths: 'Synths',
wavetables: 'Wavetables',
user: 'User', user: 'User',
importSounds: 'import-sounds', importSounds: 'import-sounds',
}} }}
@@ -110,11 +117,13 @@ export function SoundsTab() {
const params = { const params = {
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
s: name, s: name,
n: soundPreviewIdx,
clip: 1, clip: 1,
release: 0.5, release: 0.5,
sustain: 1, sustain: 1,
duration: 0.5, duration: 0.5,
}; };
soundPreviewIdx++;
const time = ctx.currentTime + 0.05; const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect(); const onended = () => trigRef.current?.node?.disconnect();
trigRef.current = Promise.resolve(onTrigger(time, params, onended)); trigRef.current = Promise.resolve(onTrigger(time, params, onended));
@@ -126,6 +135,7 @@ export function SoundsTab() {
{' '} {' '}
{name} {name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''} {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
{data?.type === 'wavetable' ? `(${getSamples(data.tables)})` : ''}
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''} {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
</span> </span>
); );
+1
View File
@@ -13,6 +13,7 @@ export const soundFilterType = {
DRUMS: 'drums', DRUMS: 'drums',
SAMPLES: 'samples', SAMPLES: 'samples',
SYNTHS: 'synths', SYNTHS: 'synths',
WAVETABLES: 'wavetables',
ALL: 'all', ALL: 'all',
}; };