mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39013c09ef | |||
| 9d425dfff6 | |||
| 6e58c973af | |||
| 2473a5391d | |||
| 4e17cfbdd6 | |||
| 83c7e63432 | |||
| 909e0154fe | |||
| ef4e21ac40 | |||
| 3c1a8c8bdb | |||
| b7e941c649 | |||
| ded91fab7d | |||
| f40d079543 | |||
| d5833dd475 | |||
| e3658ec30e | |||
| 72eaf96b4f | |||
| 9c5c71c31a | |||
| af53bab259 | |||
| 0065db8569 | |||
| 7eebf3a466 | |||
| 6f703e48e7 | |||
| 64f16c4f33 | |||
| ffbbc43c89 | |||
| 086596c47c | |||
| fdde05e751 | |||
| 694162a7b1 | |||
| cbcb25b063 | |||
| 1ae773acc4 | |||
| 0d08225983 | |||
| 00872bd1de | |||
| 120f89d57f | |||
| d496919fe5 | |||
| f369de13d7 | |||
| 99e14dae5c | |||
| ac582b4d40 | |||
| b7d98dd370 | |||
| 0ecacae71e | |||
| 8cd497abb2 | |||
| 070055d2ef | |||
| e500ebe268 | |||
| 72298c5f03 | |||
| c885d84785 | |||
| a14bf4d97b | |||
| c5f1085bd9 | |||
| ccbf28f2db | |||
| 3a7ec18d46 | |||
| a2b8407fbb | |||
| ac1fd88d8d | |||
| 9f8143e062 | |||
| cdc5e5f3f3 | |||
| bad498a1e5 | |||
| 4de0c11f8c | |||
| f360d394a7 | |||
| 48718daea8 | |||
| 8d45016ccf | |||
| ec69f6fd77 | |||
| d94f145649 | |||
| bf3fe605d9 | |||
| 54ebc97ddc | |||
| c689874bb4 | |||
| 23d4bfa9d9 | |||
| 91eb50f1c1 | |||
| 5838ade740 | |||
| 917d01c47d | |||
| 0a51718df3 | |||
| 774372a339 | |||
| 626a99ba5b | |||
| 8c3ee6db6b | |||
| c5c05ebadc | |||
| a8918d55fb | |||
| 07e3ddbe40 | |||
| a9957b45e5 | |||
| 4bd103e612 | |||
| 466234bad6 | |||
| 97beaec25a | |||
| 2b3b638943 | |||
| e1629e8383 | |||
| b3537a9acb | |||
| f0669ce3ba | |||
| 69b034349a | |||
| 5f7e10733a | |||
| 9848740e18 | |||
| 51b150b829 | |||
| 9b1c23d49f | |||
| 42ab65abdb | |||
| bb982e6b9b |
+237
-14
@@ -90,45 +90,235 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||
/**
|
||||
* Position in the wavetable of the wavetable oscillator
|
||||
*
|
||||
* @name wtPos
|
||||
* @name wt
|
||||
* @param {number | Pattern} position Position in the wavetable from 0 to 1
|
||||
* @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
|
||||
*
|
||||
* @name wtWarp
|
||||
* @name warp
|
||||
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
|
||||
* @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,
|
||||
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
|
||||
*
|
||||
* @name wtWarpMode
|
||||
* @name warpmode
|
||||
* @param {number | string | Pattern} mode Warp mode
|
||||
* @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.
|
||||
*
|
||||
* @name wtPhaseRand
|
||||
* @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');
|
||||
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.
|
||||
@@ -1697,19 +1887,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
|
||||
export const { shape } = registerControl(['shape', 'shapevol']);
|
||||
/**
|
||||
* Wave shaping distortion. CAUTION: it can get loud.
|
||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
|
||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
|
||||
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
|
||||
*
|
||||
* @name distort
|
||||
* @synonyms dist
|
||||
* @param {number | Pattern} distortion
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
* @param {number | string | Pattern} type type of distortion to apply
|
||||
* @example
|
||||
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
||||
* @example
|
||||
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
||||
* @example
|
||||
* s("bd:4*4").bank("tr808").distort("3:0.5:diode")
|
||||
*
|
||||
*/
|
||||
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
|
||||
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
|
||||
|
||||
/**
|
||||
* Postgain for waveshaping distortion.
|
||||
*
|
||||
* @name distortvol
|
||||
* @synonyms distvol
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
* @example
|
||||
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
|
||||
*/
|
||||
export const { distortvol } = registerControl('distortvol', 'distvol');
|
||||
|
||||
/**
|
||||
* Type of waveshaping distortion to apply.
|
||||
*
|
||||
* @name distorttype
|
||||
* @synonyms disttype
|
||||
* @param {number | string | Pattern} type type of distortion to apply
|
||||
* @example
|
||||
* s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>")
|
||||
*
|
||||
* @example
|
||||
* s("sine").note("F1*2").release(1)
|
||||
* .penv(24).pdecay(0.05)
|
||||
* .distort(rand.range(1, 8))
|
||||
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
|
||||
*/
|
||||
export const { distorttype } = registerControl('distorttype', 'disttype');
|
||||
|
||||
/**
|
||||
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
|
||||
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
||||
|
||||
@@ -57,7 +57,7 @@ export class Cyclist {
|
||||
}
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
|
||||
@@ -38,8 +38,7 @@ export class NeoCyclist {
|
||||
if (this.started === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||
|
||||
@@ -98,10 +98,7 @@ export class Pattern {
|
||||
|
||||
// runs func on query state
|
||||
withState(func) {
|
||||
return this.withHaps((haps, state) => {
|
||||
func(state);
|
||||
return haps;
|
||||
});
|
||||
return new Pattern((state) => this.query(func(state)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3536,3 +3533,76 @@ export const morph = (frompat, topat, bypat) => {
|
||||
bypat = reify(bypat);
|
||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||
};
|
||||
|
||||
/**
|
||||
* Soft-clipping distortion
|
||||
*
|
||||
* @name soft
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Hard-clipping distortion
|
||||
*
|
||||
* @name hard
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Cubic polynomial distortion
|
||||
*
|
||||
* @name cubic
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Diode-emulating distortion
|
||||
*
|
||||
* @name diode
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Asymmetrical diode distortion
|
||||
*
|
||||
* @name asym
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Wavefolding distortion
|
||||
*
|
||||
* @name fold
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Wavefolding distortion composed with sinusoid
|
||||
*
|
||||
* @name sinefold
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Distortion via Chebyshev polynomials
|
||||
*
|
||||
* @name chebyshev
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
||||
for (const name of distAlgoNames) {
|
||||
// Add aliases for distortion algorithms
|
||||
Pattern.prototype[name] = function (args) {
|
||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||
return this.distort(argsPat);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,7 +214,10 @@ export function repl({
|
||||
}
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = Object.values(pPatterns);
|
||||
let patterns = [];
|
||||
for (const [key, value] of Object.entries(pPatterns)) {
|
||||
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||
}
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
patterns = patterns.map((x) => eachTransform(x));
|
||||
@@ -228,6 +231,7 @@ export function repl({
|
||||
pattern = allTransforms[i](pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
|
||||
@@ -19,9 +19,9 @@ export class State {
|
||||
return this.setSpan(func(this.span));
|
||||
}
|
||||
|
||||
// Returns new State with different controls
|
||||
// Returns new State with added controls.
|
||||
setControls(controls) {
|
||||
return new State(this.span, controls);
|
||||
return new State(this.span, { ...this.controls, ...controls });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function (
|
||||
cx.connect(props);
|
||||
}
|
||||
return this.withHap((hap) => {
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
||||
let msg_topic = topic;
|
||||
if (!cx || !cx.isConnected()) {
|
||||
return;
|
||||
|
||||
@@ -20,10 +20,13 @@ export async function prebake() {
|
||||
// import('@strudel/osc'),
|
||||
);
|
||||
// load samples
|
||||
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
|
||||
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main';
|
||||
|
||||
// TODO: move this onto the strudel repo
|
||||
const ts = 'https://raw.githubusercontent.com/todepond/samples/main/';
|
||||
const ts = 'https://raw.githubusercontent.com/todepond/samples/main';
|
||||
|
||||
const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main';
|
||||
|
||||
await Promise.all([
|
||||
modulesLoading,
|
||||
registerSynthSounds(),
|
||||
@@ -36,9 +39,9 @@ export async function prebake() {
|
||||
samples(`${ds}/tidal-drum-machines.json`),
|
||||
samples(`${ds}/piano.json`),
|
||||
samples(`${ds}/Dirt-Samples.json`),
|
||||
samples(`${ds}/uzu-drumkit.json`),
|
||||
samples(`${ds}/vcsl.json`),
|
||||
samples(`${ds}/mridangam.json`),
|
||||
samples(`${tc}/strudel.json`),
|
||||
]);
|
||||
|
||||
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
||||
|
||||
@@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples')
|
||||
|
||||
The format is `github:<user>/<repo>/<branch>`.
|
||||
|
||||
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
|
||||
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
||||
|
||||
The format is also expected to be the same as explained above.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export const getAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
return setDefaultAudioContext();
|
||||
}
|
||||
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
let worklet;
|
||||
export async function dspWorklet(ac, code) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
|
||||
@@ -97,6 +98,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,
|
||||
@@ -124,6 +154,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)];
|
||||
};
|
||||
|
||||
// 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) {
|
||||
const curve = 'exponential';
|
||||
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
|
||||
@@ -317,6 +382,102 @@ export function applyFM(param, value, begin) {
|
||||
return { stop };
|
||||
}
|
||||
|
||||
// Saturation curves
|
||||
|
||||
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
|
||||
const _mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
|
||||
const _soft = (x, k) => Math.tanh(x * (1 + k));
|
||||
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
|
||||
|
||||
const _fold = (x, k) => {
|
||||
// Closed form folding for audio rate
|
||||
let y = (1 + 0.5 * k) * x;
|
||||
const window = _mod(y + 1, 4);
|
||||
return 1 - Math.abs(window - 2);
|
||||
};
|
||||
|
||||
const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k));
|
||||
|
||||
const _cubic = (x, k) => {
|
||||
const t = __squash(Math.log1p(k));
|
||||
const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1)
|
||||
return _soft(cubic, k);
|
||||
};
|
||||
|
||||
const _diode = (x, k, asym = false) => {
|
||||
const g = 1 + 2 * k; // gain
|
||||
const t = __squash(Math.log1p(k));
|
||||
const bias = 0.07 * t;
|
||||
const pos = _soft(x + bias, 2 * k);
|
||||
const neg = _soft(asym ? bias : -x + bias, 2 * k);
|
||||
const y = pos - neg;
|
||||
// We divide by the derivative at 0 so that the distortion is roughly
|
||||
// the identity map near 0 => small values are preserved and undistorted
|
||||
const sech = 1 / Math.cosh(g * bias);
|
||||
const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2
|
||||
const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x
|
||||
return _soft(y / denom, k);
|
||||
};
|
||||
|
||||
const _asym = (x, k) => _diode(x, k, true);
|
||||
|
||||
const _chebyshev = (x, k) => {
|
||||
const kl = 10 * Math.log1p(k);
|
||||
let tnm1 = 1;
|
||||
let tnm2 = x;
|
||||
let tn;
|
||||
let y = 0;
|
||||
for (let i = 1; i < 64; i++) {
|
||||
if (i < 2) {
|
||||
// Already set inital conditions
|
||||
y += i == 0 ? tnm1 : tnm2;
|
||||
continue;
|
||||
}
|
||||
tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition
|
||||
tnm2 = tnm1;
|
||||
tnm1 = tn;
|
||||
if (i % 2 === 0) {
|
||||
y += Math.min((1.3 * kl) / i, 2) * tn;
|
||||
}
|
||||
}
|
||||
// Soft clip
|
||||
return _soft(y, kl / 20);
|
||||
};
|
||||
|
||||
export const distortionAlgorithms = {
|
||||
scurve: _scurve,
|
||||
soft: _soft,
|
||||
hard: _hard,
|
||||
cubic: _cubic,
|
||||
diode: _diode,
|
||||
asym: _asym,
|
||||
fold: _fold,
|
||||
sinefold: _sineFold,
|
||||
chebyshev: _chebyshev,
|
||||
};
|
||||
const _algoNames = Object.freeze(Object.keys(distortionAlgorithms));
|
||||
|
||||
export const getDistortionAlgorithm = (algo) => {
|
||||
let index = algo;
|
||||
if (typeof algo === 'string') {
|
||||
index = _algoNames.indexOf(algo);
|
||||
if (index === -1) {
|
||||
logger(`[superdough] Could not find waveshaping algorithm ${algo}.
|
||||
Available options are ${_algoNames.join(', ')}.
|
||||
Defaulting to ${_algoNames[0]}.`);
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number
|
||||
return distortionAlgorithms[name];
|
||||
};
|
||||
|
||||
export const getDistortion = (distort, postgain, algorithm) => {
|
||||
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
||||
};
|
||||
|
||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||
let { note, freq } = value;
|
||||
note = note || defaultNote;
|
||||
|
||||
@@ -11,4 +11,5 @@ export * from './synth.mjs';
|
||||
export * from './zzfx.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './dspworklet.mjs';
|
||||
export * from './audioContext.mjs';
|
||||
export * from './wavetable.mjs';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { drywet } from './helpers.mjs';
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
let noiseCache = {};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getCommonSampleInfoFromBank } from './util.mjs';
|
||||
import { registerSound, registerWaveTable } from './index.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
@@ -22,71 +23,47 @@ function humanFileSize(bytes, si) {
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
// 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)
|
||||
export function getSampleInfo(hapValue, bank) {
|
||||
const { s, n = 0, speed = 1.0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
let sampleUrl;
|
||||
let index = 0;
|
||||
if (Array.isArray(bank)) {
|
||||
index = getSoundIndex(n, bank.length);
|
||||
sampleUrl = bank[index];
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
.filter((k) => !k.startsWith('_'))
|
||||
.reduce(
|
||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||
null,
|
||||
);
|
||||
transpose = -midiDiff(closest); // semitones to repitch
|
||||
index = getSoundIndex(n, bank[closest].length);
|
||||
sampleUrl = bank[closest][index];
|
||||
}
|
||||
const label = `${s}:${index}`;
|
||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
return { transpose, sampleUrl, index, midi, label, playbackRate };
|
||||
}
|
||||
|
||||
// takes hapValue and returns buffer + playbackRate.
|
||||
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => {
|
||||
if (resolveUrl) {
|
||||
sampleUrl = await resolveUrl(sampleUrl);
|
||||
}
|
||||
const ac = getAudioContext();
|
||||
const buffer = await loadBuffer(sampleUrl, ac, label);
|
||||
return buffer
|
||||
};
|
||||
|
||||
function getBufferPlaybackRate(hapValue, buffer, transpose) {
|
||||
const { speed = 1.0 } = hapValue;
|
||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
if (hapValue.unit === 'c') {
|
||||
playbackRate = playbackRate * buffer.duration;
|
||||
}
|
||||
return { buffer, playbackRate };
|
||||
};
|
||||
return playbackRate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// creates playback ready AudioBufferSourceNode from hapValue
|
||||
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
|
||||
export function getSampleBufferSource(hapValue, buffer,transpose) {
|
||||
if (hapValue.speed < 0) {
|
||||
// should this be cached?
|
||||
buffer = reverseBuffer(buffer);
|
||||
}
|
||||
const playbackRate = getBufferPlaybackRate(hapValue, buffer, transpose)
|
||||
const ac = getAudioContext();
|
||||
const bufferSource = ac.createBufferSource();
|
||||
bufferSource.buffer = buffer;
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
|
||||
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
|
||||
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
|
||||
const loop = hapValue.loop;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
@@ -143,13 +120,20 @@ function githubPath(base, subpath = '') {
|
||||
if (!base.startsWith('github:')) {
|
||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||
}
|
||||
let [_, path] = base.split('github:');
|
||||
let path = base.slice('github:'.length);
|
||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
if (path.split('/').length === 2) {
|
||||
// assume main as default branch if none set
|
||||
path += '/main';
|
||||
|
||||
let components = path.split('/');
|
||||
let user = components[0];
|
||||
let repo = components.length >= 2 ? components[1] : 'samples';
|
||||
let branch = components.length >= 3 ? components[2] : 'main';
|
||||
let other = components.slice(3);
|
||||
if (subpath) {
|
||||
other.push(subpath);
|
||||
}
|
||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||
other = other.join('/');
|
||||
|
||||
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
||||
}
|
||||
|
||||
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||
@@ -267,23 +251,19 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
||||
return samples(json, baseUrl || base, options);
|
||||
}
|
||||
const { prebake, tag } = options;
|
||||
|
||||
processSampleMap(
|
||||
sampleMap,
|
||||
(key, bank) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||
type: 'sample',
|
||||
samples: bank,
|
||||
baseUrl,
|
||||
prebake,
|
||||
tag,
|
||||
}),
|
||||
(key, bank) => {
|
||||
registerSampleSource(key, bank, { baseUrl, prebake, tag });
|
||||
},
|
||||
baseUrl,
|
||||
);
|
||||
};
|
||||
|
||||
const cutGroups = [];
|
||||
|
||||
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
export async function onTriggerSample(t, value, onended, bufferSrc) {
|
||||
let {
|
||||
s,
|
||||
nudge = 0, // TODO: is this in seconds?
|
||||
@@ -305,7 +285,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||
|
||||
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
||||
const { bufferSource, sliceDuration, offset } = bufferSrc
|
||||
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
@@ -366,3 +346,27 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
async function getBufferSrcFromBank(hapValue, bank, resolveUrl = undefined) {
|
||||
const { transpose, url, label } = getCommonSampleInfoFromBank(hapValue, bank)
|
||||
let buffer = await getSampleBuffer(label,url, resolveUrl);
|
||||
return getSampleBufferSource(hapValue, buffer, transpose)
|
||||
}
|
||||
|
||||
function registerSample(key, bank, params) {
|
||||
registerSound(key, async (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined)), {
|
||||
type: 'sample',
|
||||
samples: bank,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function registerSampleSource(key, bank, params) {
|
||||
const isWavetable = key.startsWith('wt_');
|
||||
if (isWavetable) {
|
||||
registerWaveTable(key, bank, params);
|
||||
} else {
|
||||
registerSample(key, bank, params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ 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, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||
|
||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||
@@ -29,13 +30,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 = {}) {
|
||||
@@ -161,6 +155,7 @@ let defaultDefaultValues = {
|
||||
phaserdepth: 0.75,
|
||||
shapevol: 1,
|
||||
distortvol: 1,
|
||||
distorttype: 0,
|
||||
delay: 0,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
@@ -207,25 +202,6 @@ export function setVersionDefaults(version) {
|
||||
|
||||
export const resetLoadedSounds = () => soundMap.set({});
|
||||
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext({ latencyHint: 'playback' });
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export const getAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
return setDefaultAudioContext();
|
||||
}
|
||||
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
|
||||
let externalWorklets = [];
|
||||
export function registerWorklet(url) {
|
||||
externalWorklets.push(url);
|
||||
@@ -314,28 +290,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 });
|
||||
@@ -502,6 +456,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
shapevol = getDefaultValue('shapevol'),
|
||||
distort,
|
||||
distortvol = getDefaultValue('distortvol'),
|
||||
distorttype = getDefaultValue('distorttype'),
|
||||
pan,
|
||||
vowel,
|
||||
delay = getDefaultValue('delay'),
|
||||
@@ -584,7 +539,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
audioNodes.forEach((n) => n?.disconnect());
|
||||
activeSoundSources.delete(chainID);
|
||||
};
|
||||
const soundHandle = await onTrigger(t, value, onEnded);
|
||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
@@ -675,13 +630,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
// effects
|
||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||
|
||||
if (tremolosync != null) {
|
||||
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
|
||||
|
||||
@@ -135,12 +135,14 @@ export class SuperdoughOutput {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.disconnect();
|
||||
this.initializeAudio();
|
||||
}
|
||||
disconnect() {
|
||||
this.channelMerger.disconnect();
|
||||
this.destinationGain.disconnect();
|
||||
this.destinationGain = null;
|
||||
this.channelMerger = null;
|
||||
this.nodes = {};
|
||||
this.initializeAudio();
|
||||
}
|
||||
connectToDestination = (input, channels = [0, 1]) => {
|
||||
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||
@@ -172,6 +174,7 @@ export class SuperdoughAudioController {
|
||||
Array.from(this.nodes).forEach((node) => {
|
||||
node.disconnect();
|
||||
});
|
||||
this.nodes = {};
|
||||
this.output.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { clamp } from './util.mjs';
|
||||
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
|
||||
import { registerSound, soundMap } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
destroyAudioWorkletNode,
|
||||
gainNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
|
||||
@@ -76,3 +76,47 @@ 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)
|
||||
export function getCommonSampleInfoFromBank(hapValue, bank) {
|
||||
const { s, n = 0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let url;
|
||||
let index = 0;
|
||||
let {transpose, label} = getCommonSampleInfo(hapValue)
|
||||
|
||||
if (Array.isArray(bank)) {
|
||||
index = getSoundIndex(n, bank.length);
|
||||
url = bank[index];
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
.filter((k) => !k.startsWith('_'))
|
||||
.reduce(
|
||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||
null,
|
||||
);
|
||||
transpose = -midiDiff(closest); // semitones to repitch
|
||||
index = getSoundIndex(n, bank[closest].length);
|
||||
url = bank[closest][index];
|
||||
}
|
||||
label = `${s}:${index}`;
|
||||
return { transpose, url, label };
|
||||
}
|
||||
|
||||
|
||||
export function getCommonSampleInfo(hapValue) {
|
||||
const { s, n = 0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
const label = `${s}:${n}`;
|
||||
return { transpose, label };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getSoundIndex, valueToMidi } from './util.mjs';
|
||||
import { getCommonSampleInfoFromBank } from './util.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
applyParameterModulators,
|
||||
destroyAudioWorkletNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
@@ -12,8 +14,7 @@ import {
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
const WT_MAX_MIP_LEVELS = 6;
|
||||
export const WarpMode = Object.freeze({
|
||||
export const Warpmode = Object.freeze({
|
||||
NONE: 0,
|
||||
ASYM: 1,
|
||||
MIRROR: 2,
|
||||
@@ -38,40 +39,25 @@ export const WarpMode = Object.freeze({
|
||||
FLIP: 21,
|
||||
});
|
||||
|
||||
async function loadWavetableFrames(url, label, frameLen = 256) {
|
||||
const ac = getAudioContext();
|
||||
const buf = await loadBuffer(url, ac, label);
|
||||
const ch0 = buf.getChannelData(0);
|
||||
const total = ch0.length;
|
||||
const numFrames = Math.floor(total / frameLen);
|
||||
const frames = new Array(numFrames);
|
||||
for (let i = 0; i < numFrames; i++) {
|
||||
const start = i * frameLen;
|
||||
frames[i] = ch0.subarray(start, start + frameLen);
|
||||
const seenKeys = new Set();
|
||||
async function getPayload(url, label, frameLen = 2048) {
|
||||
const key = `${url},${frameLen}`;
|
||||
if (!seenKeys.has(key)) {
|
||||
const buf = await loadBuffer(url, label);
|
||||
const ch0 = buf.getChannelData(0);
|
||||
const total = ch0.length;
|
||||
const numFrames = Math.max(1, Math.floor(total / frameLen));
|
||||
const frames = new Array(numFrames);
|
||||
for (let i = 0; i < numFrames; i++) {
|
||||
const start = i * frameLen;
|
||||
frames[i] = ch0.subarray(start, start + frameLen);
|
||||
}
|
||||
seenKeys.add(key);
|
||||
return { frames, frameLen, numFrames, key };
|
||||
}
|
||||
|
||||
// build mipmaps
|
||||
const mipmaps = [frames];
|
||||
let levelFrames = frames;
|
||||
for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) {
|
||||
const prevLen = levelFrames[0].length;
|
||||
if (prevLen <= 32) break;
|
||||
const nextLen = prevLen >> 1;
|
||||
const next = levelFrames.map((src) => {
|
||||
const out = new Float32Array(nextLen);
|
||||
for (let j = 0; j < nextLen; j++) {
|
||||
out[j] = (src[2 * j] + src[2 * j + 1]) / 2;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
mipmaps.push(next);
|
||||
levelFrames = next;
|
||||
}
|
||||
return { frames, mipmaps, frameLen, numFrames };
|
||||
return { frameLen, key }; // worklet will use the cached version
|
||||
}
|
||||
|
||||
const loadCache = {};
|
||||
|
||||
function humanFileSize(bytes, si) {
|
||||
var thresh = si ? 1000 : 1024;
|
||||
if (bytes < thresh) return bytes + ' B';
|
||||
@@ -86,17 +72,38 @@ function humanFileSize(bytes, si) {
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
export function getTableInfo(hapValue, tableUrls) {
|
||||
const { s, n = 0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
const index = getSoundIndex(n, tableUrls.length);
|
||||
const tableUrl = tableUrls[index];
|
||||
const label = `${s}:${index}`;
|
||||
return { transpose, tableUrl, index, midi, label };
|
||||
// Extract the sample rate of a .wav file
|
||||
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 loadCache = {};
|
||||
const loadBuffer = (url, label) => {
|
||||
url = url.replace('#', '%23');
|
||||
if (!loadCache[url]) {
|
||||
logger(`[wavetable] load table ${label}..`, 'load-table', { url });
|
||||
@@ -107,7 +114,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;
|
||||
});
|
||||
}
|
||||
@@ -127,34 +134,55 @@ 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);
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, value, frameLen), {
|
||||
type: 'wavetable',
|
||||
tables: value,
|
||||
baseUrl,
|
||||
frameLen,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function registerWaveTable(key, tables, params) {
|
||||
registerSound(
|
||||
key,
|
||||
(t, hapValue, onended, cps) => {
|
||||
return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048);
|
||||
},
|
||||
{
|
||||
type: 'wavetable',
|
||||
tables,
|
||||
...params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection of wavetables to use with `s`
|
||||
*
|
||||
* @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');
|
||||
@@ -172,26 +200,30 @@ 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}"`);
|
||||
});
|
||||
};
|
||||
|
||||
async function onTriggerSynth(t, value, onended, bank, frameLen) {
|
||||
const { s, n = 0, duration } = value;
|
||||
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
const { s, n = 0, duration, clip } = value;
|
||||
const ac = getAudioContext();
|
||||
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||
let { wtWarpMode } = value;
|
||||
if (typeof wtWarpMode === 'string') {
|
||||
wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE;
|
||||
let { warpmode } = value;
|
||||
if (typeof warpmode === 'string') {
|
||||
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
||||
}
|
||||
const frequency = getFrequencyFromValue(value);
|
||||
const { tableUrl, label } = getTableInfo(value, bank);
|
||||
const payload = await loadWavetableFrames(tableUrl, label, frameLen);
|
||||
const holdEnd = t + duration;
|
||||
const envEnd = holdEnd + release + 0.01;
|
||||
const { url, label } = getCommonSampleInfoFromBank(value, tables);
|
||||
const payload = await getPayload(url, label, frameLen);
|
||||
let holdEnd = t + duration;
|
||||
if (clip !== undefined) {
|
||||
holdEnd = Math.min(t + clip * duration, holdEnd);
|
||||
}
|
||||
const endWithRelease = holdEnd + release;
|
||||
const envEnd = endWithRelease + 0.01;
|
||||
const source = getWorklet(
|
||||
ac,
|
||||
'wavetable-oscillator-processor',
|
||||
@@ -199,34 +231,99 @@ async function onTriggerSynth(t, value, onended, bank, frameLen) {
|
||||
begin: t,
|
||||
end: envEnd,
|
||||
frequency,
|
||||
detune: value.detune,
|
||||
position: value.wtPos,
|
||||
warp: value.wtWarp,
|
||||
warpMode: wtWarpMode,
|
||||
voices: value.unison,
|
||||
spread: value.spread,
|
||||
phaserand: value.wtPhaseRand,
|
||||
freqspread: value.detune,
|
||||
position: value.wt,
|
||||
warp: value.warp,
|
||||
warpMode: warpmode,
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
panspread: value.spread,
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
},
|
||||
{ outputChannelCount: [2] },
|
||||
);
|
||||
source.port.postMessage({ type: 'tables', payload });
|
||||
source.port.postMessage({ type: 'table', payload });
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||
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 fm = applyFM(source.parameters.get('frequency'), value, t);
|
||||
const envGain = ac.createGain();
|
||||
const node = source.connect(envGain);
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||
getPitchEnvelope(source.detune, value, t, holdEnd);
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||
const handle = { node, source };
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
source.disconnect();
|
||||
destroyAudioWorkletNode(source);
|
||||
vibratoOscillator?.stop();
|
||||
fm?.stop();
|
||||
node.disconnect();
|
||||
wtPosModulators?.disconnect();
|
||||
wtWarpModulators?.disconnect();
|
||||
onended();
|
||||
},
|
||||
t,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import OLAProcessor from './ola-processor';
|
||||
import FFT from './fft.js';
|
||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
const mod = (n, m) => ((n % m) + m) % m;
|
||||
@@ -137,7 +138,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
process(_inputs, outputs, parameters) {
|
||||
const begin = parameters['begin'][0];
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
return false;
|
||||
@@ -430,9 +431,10 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
constructor({ processorOptions }) {
|
||||
super();
|
||||
this.started = false;
|
||||
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
@@ -444,13 +446,12 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||
return false;
|
||||
}
|
||||
this.started = hasInput;
|
||||
|
||||
const shape = Math.expm1(parameters.distort[0]);
|
||||
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
|
||||
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
|
||||
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
||||
const shape = Math.expm1(pv(parameters.distort, n));
|
||||
for (let ch = 0; ch < input.length; ch++) {
|
||||
const x = input[ch][n];
|
||||
output[ch][n] = postgain * this.algorithm(x, shape);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -510,7 +511,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
},
|
||||
];
|
||||
}
|
||||
process(input, outputs, params) {
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
@@ -1043,49 +1044,60 @@ function brownian(x, oct = 4) {
|
||||
return (sum / norm) * 2 - 1;
|
||||
}
|
||||
|
||||
const tablesCache = {};
|
||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
|
||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||
{ name: 'detune', defaultValue: 0 },
|
||||
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warp', defaultValue: 0, min: 0, max: 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: 'voices', defaultValue: 1, min: 1 },
|
||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.tables = null;
|
||||
this.frameLen = 0;
|
||||
this.numFrames = 0;
|
||||
this.phase = [];
|
||||
this.syncRatio = 1;
|
||||
this.invSR = 1 / sampleRate;
|
||||
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'tables') {
|
||||
this.tables = payload.mipmaps;
|
||||
if (type === 'table') {
|
||||
const key = payload.key;
|
||||
this.frameLen = payload.frameLen;
|
||||
if (!tablesCache[key]) {
|
||||
const tables = [payload.frames];
|
||||
let table = tables[0];
|
||||
for (let level = 1; level < 1; level++) {
|
||||
const nextLen = table.length >> 1;
|
||||
const nextTable = table.map((frame) => {
|
||||
const avg = new Float32Array(nextLen);
|
||||
for (let i = 0; i < nextLen; i++) {
|
||||
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
|
||||
}
|
||||
return avg;
|
||||
});
|
||||
tables.push(nextTable);
|
||||
table = nextTable;
|
||||
if (nextLen <= 32) break;
|
||||
}
|
||||
tablesCache[key] = tables;
|
||||
}
|
||||
this.tables = tablesCache[key];
|
||||
this.numFrames = this.tables[0].length;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_chooseMip(dphi) {
|
||||
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
||||
let level = 0;
|
||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||
level++;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
_mirror(x) {
|
||||
return 1 - Math.abs(2 * x - 1);
|
||||
}
|
||||
@@ -1222,14 +1234,27 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
_sampleFrame(frame, phase) {
|
||||
const pos = phase * (frame.length - 1);
|
||||
const i = pos | 0;
|
||||
const len = frame.length;
|
||||
const pos = phase * len;
|
||||
let i = pos | 0;
|
||||
if (i >= len) i = 0; // fast wrap
|
||||
const frac = pos - i;
|
||||
const a = frame[i];
|
||||
const b = frame[(i + 1) % frame.length];
|
||||
let i1 = i + 1;
|
||||
if (i1 >= len) i1 = 0;
|
||||
const b = frame[i1];
|
||||
return a + (b - a) * frac;
|
||||
}
|
||||
|
||||
_chooseMip(dphi) {
|
||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
||||
let level = 0;
|
||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||
level++;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
return false;
|
||||
@@ -1239,28 +1264,28 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
const outL = outputs[0][0];
|
||||
const outR = outputs[0][1] || outputs[0][0];
|
||||
|
||||
if (!this.tables) {
|
||||
outL.fill(0);
|
||||
if (outR !== outL) outR.set(outL);
|
||||
return true;
|
||||
}
|
||||
|
||||
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 freqspread = pv(parameters.freqspread, i);
|
||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||
const idx = tablePos * (this.numFrames - 1);
|
||||
const fIdx = idx | 0;
|
||||
const frac = idx - fIdx;
|
||||
const warpAmount = pv(parameters.warp, i);
|
||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||
const warpMode = pv(parameters.warpMode, i);
|
||||
const voices = pv(parameters.voices, i);
|
||||
const phaseRand = pv(parameters.phaserand, i);
|
||||
const gain1 = Math.sqrt(1 - spread);
|
||||
const gain2 = Math.sqrt(spread);
|
||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||
let f = pv(parameters.frequency, i);
|
||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||
const normalizer = 1 / Math.sqrt(voices);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const isOdd = (n & 1) == 1;
|
||||
let gainL = gain1;
|
||||
@@ -1270,22 +1295,22 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
|
||||
const dPhase = fVoice / sampleRate;
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
|
||||
const dPhase = fVoice * this.invSR;
|
||||
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);
|
||||
outR[i] += (s * gainR) / Math.sqrt(voices);
|
||||
outL[i] += s * gainL * normalizer;
|
||||
outR[i] += s * gainR * normalizer;
|
||||
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//import { ZZFX } from 'zzfx';
|
||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||
import { registerSound } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { buildSamples } from './zzfx_fork.mjs';
|
||||
|
||||
export const getZZFX = (value, t) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
||||
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
||||
|
||||
@@ -2829,6 +2829,82 @@ exports[`runs examples > example "distort" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distort" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 1/4 → 1/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 1/2 → 3/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 3/4 → 1/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 1/1 → 5/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 5/4 → 3/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 3/2 → 7/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 7/4 → 2/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 2/1 → 9/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 9/4 → 5/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 5/2 → 11/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 11/4 → 3/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 3/1 → 13/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 13/4 → 7/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 7/2 → 15/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 15/4 → 4/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distorttype" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distorttype" example index 1 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]",
|
||||
"[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]",
|
||||
"[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]",
|
||||
"[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]",
|
||||
"[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]",
|
||||
"[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]",
|
||||
"[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]",
|
||||
"[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distortvol" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "djf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
|
||||
@@ -11789,6 +11865,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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/5 | note:c2 s:sine ]",
|
||||
@@ -11993,6 +12175,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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh gain:0 ]",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/",
|
||||
"wt_digital": [
|
||||
"wt_digital/wt_bad_day.wav",
|
||||
"wt_digital/wt_basique.wav",
|
||||
"wt_digital/wt_crickets.wav",
|
||||
"wt_digital/wt_curses.wav",
|
||||
"wt_digital/wt_echoes.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_echoes": ["wt_digital/wt_echoes.wav"],
|
||||
"wt_vgame": [
|
||||
"wt_vgame/wt_vgame10.wav",
|
||||
"wt_vgame/wt_vgame11.wav",
|
||||
"wt_vgame/wt_vgame12.wav",
|
||||
"wt_vgame/wt_vgame13.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"
|
||||
]
|
||||
}
|
||||
@@ -60,6 +60,23 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
|
||||
.room(0.5)`}
|
||||
/>
|
||||
|
||||
## Write your own chained function
|
||||
|
||||
You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const effectChain = register('effectChain', (pat) => pat
|
||||
.s("sawtooth")
|
||||
.cutoff(500)
|
||||
//.delay(0.5)
|
||||
.room(0.5)
|
||||
)
|
||||
note("a3 c#4 e4 a4").effectChain()`}
|
||||
/>
|
||||
|
||||
Try adding `.rev()` after `effectChain()` to hear further effects added.
|
||||
|
||||
# Comments
|
||||
|
||||
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
||||
|
||||
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
||||
|
||||
### scrub
|
||||
|
||||
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
|
||||
<JsDoc client:idle name="Pattern.scrub" h={0} />
|
||||
|
||||
### speed
|
||||
|
||||
|
||||
@@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro
|
||||
|
||||
This Guide shows you the different ways to get started with using Strudel in your own project.
|
||||
|
||||
## Respect the license
|
||||
|
||||
First, please take a moment to understand Strudel's free/open source license,
|
||||
[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
||||
|
||||
Here is a lay summary, but check the license for legal definitions and responsibilities.
|
||||
|
||||
- You can distribute modified versions if you keep track of the changes and the date you made them.
|
||||
- You must license derivative work under the same license.
|
||||
- Source code must be distributed along with web publication.
|
||||
|
||||
Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license.
|
||||
|
||||
This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
|
||||
|
||||
## Embedding the Strudel REPL
|
||||
|
||||
There are 3 quick ways to embed strudel in your website:
|
||||
|
||||
@@ -44,10 +44,10 @@ export function Reference() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lowCaseSearch = search.toLowerCase();
|
||||
const lowerCaseSearch = search.toLowerCase();
|
||||
return (
|
||||
entry.name.toLowerCase().includes(lowCaseSearch) ||
|
||||
(entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false)
|
||||
entry.name.toLowerCase().includes(lowerCaseSearch) ||
|
||||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
|
||||
);
|
||||
});
|
||||
}, [search]);
|
||||
|
||||
@@ -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})` : ''}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { registerSound, onTriggerSample } from '@strudel/webaudio';
|
||||
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio';
|
||||
import { isAudioFile } from './files.mjs';
|
||||
import { logger } from '@strudel/core';
|
||||
import { getSoundIndex, logger } from '@strudel/core';
|
||||
import { registerSound } from '@strudel/webaudio';
|
||||
import { getCommonSampleInfo } from '../../../packages/superdough/util.mjs';
|
||||
|
||||
//utilites for writing and reading to the indexdb
|
||||
|
||||
@@ -25,6 +27,15 @@ function clearAllIDB() {
|
||||
|
||||
export function clearIDB(dbName) {
|
||||
return window.indexedDB.deleteDatabase(dbName);
|
||||
}
|
||||
|
||||
|
||||
function registerSampleFromIdb(key, bank, params) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// queries the DB, and registers the sounds so they can be played
|
||||
@@ -52,37 +63,76 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const splitRelativePath = soundFile.id.split('/');
|
||||
let parentDirectory =
|
||||
//fallback to file name before period and seperator if no parent directory
|
||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||
const blob = soundFile.blob;
|
||||
|
||||
return blobToDataUrl(blob).then((soundPath) => {
|
||||
|
||||
// const blob = soundFile.blob;
|
||||
|
||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||
|
||||
titlePathMap.set(title, soundPath);
|
||||
|
||||
titlePathMap.set(title, soundFile.id);
|
||||
sounds.set(parentDirectory, titlePathMap);
|
||||
return;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// return blobToDataUrl(blob).then((soundPath) => {
|
||||
// const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||
|
||||
// titlePathMap.set(title, soundPath);
|
||||
|
||||
// sounds.set(parentDirectory, titlePathMap);
|
||||
// return;
|
||||
// });
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
sounds.forEach((titlePathMap, key) => {
|
||||
const value = Array.from(titlePathMap.keys())
|
||||
const bank = Array.from(titlePathMap.keys())
|
||||
.sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map((title) => titlePathMap.get(title));
|
||||
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
baseUrl: undefined,
|
||||
prebake: false,
|
||||
tag: undefined,
|
||||
});
|
||||
|
||||
|
||||
|
||||
registerSound(key, async (t, hapValue, onended) => {
|
||||
const { s, n = 0 } = hapValue;
|
||||
const index = getSoundIndex(n, bank.length);
|
||||
let {transpose, label} = getCommonSampleInfo(hapValue)
|
||||
const storeKey = bank[index];
|
||||
|
||||
openDB(config, (objectStore) => {
|
||||
const getRequest = objectStore.get(storeKey);
|
||||
getRequest.onsuccess = async (event) => {
|
||||
const result = event.target.result;
|
||||
let buffer = result?.blob.arrayBuffer ? await result.blob.arrayBuffer() : null;
|
||||
console.info(buffer)
|
||||
if (buffer) {
|
||||
const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
|
||||
onTriggerSample(t, hapValue, onended, bufferSource)
|
||||
} else {
|
||||
logger(`Could not load sample for ${storeKey}`, 'error');
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
// const buffer = objectStore.getKey(key)
|
||||
// const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
|
||||
|
||||
// onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined))
|
||||
|
||||
}, {
|
||||
type: 'sample',
|
||||
samples: bank,
|
||||
|
||||
});
|
||||
|
||||
// registerSampleSource(key, value, { prebake: false });
|
||||
});
|
||||
|
||||
logger('imported sounds registered!', 'success');
|
||||
|
||||
@@ -32,6 +32,9 @@ export async function prebake() {
|
||||
prebake: true,
|
||||
tag: 'drum-machines',
|
||||
}),
|
||||
samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
|
||||
prebake: true,
|
||||
}),
|
||||
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
|
||||
samples(
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ export const soundFilterType = {
|
||||
DRUMS: 'drums',
|
||||
SAMPLES: 'samples',
|
||||
SYNTHS: 'synths',
|
||||
WAVETABLES: 'wavetables',
|
||||
ALL: 'all',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user