mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-27 23:15:12 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bcd5897da | |||
| a7adebe036 | |||
| 62379269f0 | |||
| 1162a42218 | |||
| 32d11d0332 | |||
| e5eb3b8d7b |
@@ -1887,52 +1887,19 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
|
|||||||
export const { shape } = registerControl(['shape', 'shapevol']);
|
export const { shape } = registerControl(['shape', 'shapevol']);
|
||||||
/**
|
/**
|
||||||
* Wave shaping distortion. CAUTION: it can get loud.
|
* Wave shaping distortion. CAUTION: it can get loud.
|
||||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
|
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
|
||||||
* 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 ;)
|
* 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
|
* @name distort
|
||||||
* @synonyms dist
|
* @synonyms dist
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
|
||||||
* @param {number | string | Pattern} type type of distortion to apply
|
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
||||||
* @example
|
* @example
|
||||||
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
* 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', 'distorttype'], 'dist');
|
export const { distort, dist } = registerControl(['distort', 'distortvol'], '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")`
|
* 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)
|
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
||||||
|
|||||||
+51
-68
@@ -8,6 +8,7 @@ import TimeSpan from './timespan.mjs';
|
|||||||
import Fraction, { isFraction, lcm } from './fraction.mjs';
|
import Fraction, { isFraction, lcm } from './fraction.mjs';
|
||||||
import Hap from './hap.mjs';
|
import Hap from './hap.mjs';
|
||||||
import State from './state.mjs';
|
import State from './state.mjs';
|
||||||
|
import { getTime } from './time.mjs';
|
||||||
import { unionWithObj } from './value.mjs';
|
import { unionWithObj } from './value.mjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -3534,75 +3535,57 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TIMELINES = {
|
||||||
|
state: {},
|
||||||
|
polarities: {},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Soft-clipping distortion
|
* Aligns the pattern with the specified `timeline` by ID.
|
||||||
*
|
*
|
||||||
* @name soft
|
* More specifically, if a timeline ID is encountered for the first time, that moment
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* in time is marked as the "start" of that pattern. Thereafter, any other patterns aligned
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* with that same id will begin at that same moment in time.
|
||||||
*
|
*
|
||||||
|
* If the sign of the id changes (positive to negative or vice versa), we reset the timeline.
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} id Timeline id. Must be a non-zero number. Switch signs to reset.
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("tri").seg(8).n(irand(12)).scale("G#:minor").lpf(400).room(2)
|
||||||
|
* .timeline("<1 -1 1 2>")
|
||||||
|
* @example
|
||||||
|
* $: n("[0 .. 6]/4").scale("F:minor")
|
||||||
|
* // Execute the following line at any point in the session
|
||||||
|
* // and it will always start on note 0
|
||||||
|
* // $: n("[0 .. 6]/4").scale("F:minor").timeline(2)
|
||||||
*/
|
*/
|
||||||
/**
|
export const timeline = register('timeline', (id, pat) => {
|
||||||
* Hard-clipping distortion
|
if (typeof id !== 'number' || id === 0) {
|
||||||
*
|
logger(
|
||||||
* @name hard
|
`[query] ${id} is not a valid timeline id. Please ensure it is a non-zero number. Defaulting to timeline 1.`,
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
);
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
id = 1;
|
||||||
*
|
}
|
||||||
*/
|
const { state, polarities } = TIMELINES;
|
||||||
/**
|
const key = Math.abs(id);
|
||||||
* Cubic polynomial distortion
|
// We let the pattern run until the sign switches
|
||||||
*
|
const p = id > 0 ? 1 : -1;
|
||||||
* @name cubic
|
let t = polarities[key] === p ? state[key] : Math.ceil(getTime());
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
return (
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
pat
|
||||||
*
|
.late(t)
|
||||||
*/
|
.onTrigger((hap) => {
|
||||||
/**
|
const T = Number(hap.part.begin);
|
||||||
* Diode-emulating distortion
|
// Set state on the first trigger
|
||||||
*
|
state[key] ??= T;
|
||||||
* @name diode
|
polarities[key] ??= p;
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
if (polarities[key] !== p) {
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
state[key] = T;
|
||||||
*
|
}
|
||||||
*/
|
polarities[key] = p;
|
||||||
/**
|
}, false)
|
||||||
* Asymmetrical diode distortion
|
// Add labels
|
||||||
*
|
.withValue((v) => ({ ...v, timeline: id, offset: t }))
|
||||||
* @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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { evaluate as _evaluate } from './evaluate.mjs';
|
|||||||
import { errorLogger, logger } from './logger.mjs';
|
import { errorLogger, logger } from './logger.mjs';
|
||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack, TIMELINES } from './pattern.mjs';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
@@ -52,6 +52,11 @@ export function repl({
|
|||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
onToggle?.(started);
|
onToggle?.(started);
|
||||||
|
if (!started) {
|
||||||
|
// Reset timeline state
|
||||||
|
TIMELINES.state = {};
|
||||||
|
TIMELINES.polarities = {};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setInterval,
|
setInterval,
|
||||||
clearInterval,
|
clearInterval,
|
||||||
|
|||||||
@@ -20,13 +20,10 @@ export async function prebake() {
|
|||||||
// import('@strudel/osc'),
|
// import('@strudel/osc'),
|
||||||
);
|
);
|
||||||
// load samples
|
// 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
|
// 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([
|
await Promise.all([
|
||||||
modulesLoading,
|
modulesLoading,
|
||||||
registerSynthSounds(),
|
registerSynthSounds(),
|
||||||
@@ -39,9 +36,9 @@ export async function prebake() {
|
|||||||
samples(`${ds}/tidal-drum-machines.json`),
|
samples(`${ds}/tidal-drum-machines.json`),
|
||||||
samples(`${ds}/piano.json`),
|
samples(`${ds}/piano.json`),
|
||||||
samples(`${ds}/Dirt-Samples.json`),
|
samples(`${ds}/Dirt-Samples.json`),
|
||||||
|
samples(`${ds}/uzu-drumkit.json`),
|
||||||
samples(`${ds}/vcsl.json`),
|
samples(`${ds}/vcsl.json`),
|
||||||
samples(`${ds}/mridangam.json`),
|
samples(`${ds}/mridangam.json`),
|
||||||
samples(`${tc}/strudel.json`),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ samples('github:tidalcycles/dirt-samples')
|
|||||||
|
|
||||||
The format is `github:<user>/<repo>/<branch>`.
|
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.
|
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.
|
The format is also expected to be the same as explained above.
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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 './audioContext.mjs';
|
import { getAudioContext } from './superdough.mjs';
|
||||||
|
|
||||||
let worklet;
|
let worklet;
|
||||||
export async function dspWorklet(ac, code) {
|
export async function dspWorklet(ac, code) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './superdough.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
import { logger } from './logger.mjs';
|
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
||||||
@@ -382,102 +381,6 @@ export function applyFM(param, value, begin) {
|
|||||||
return { stop };
|
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) => {
|
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||||
let { note, freq } = value;
|
let { note, freq } = value;
|
||||||
note = note || defaultNote;
|
note = note || defaultNote;
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ export * from './synth.mjs';
|
|||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
export * from './audioContext.mjs';
|
|
||||||
export * from './wavetable.mjs';
|
export * from './wavetable.mjs';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { drywet } from './helpers.mjs';
|
import { drywet } from './helpers.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './superdough.mjs';
|
||||||
|
|
||||||
let noiseCache = {};
|
let noiseCache = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { getCommonSampleInfoFromBank } from './util.mjs';
|
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
|
||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
@@ -23,34 +22,35 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSampleInfo(hapValue, bank) {
|
||||||
|
const { speed = 1.0 } = hapValue;
|
||||||
|
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||||
|
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
|
}
|
||||||
|
|
||||||
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => {
|
// takes hapValue and returns buffer + playbackRate.
|
||||||
|
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||||
|
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||||
if (resolveUrl) {
|
if (resolveUrl) {
|
||||||
sampleUrl = await resolveUrl(sampleUrl);
|
sampleUrl = await resolveUrl(sampleUrl);
|
||||||
}
|
}
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const buffer = await loadBuffer(sampleUrl, ac, label);
|
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') {
|
if (hapValue.unit === 'c') {
|
||||||
playbackRate = playbackRate * buffer.duration;
|
playbackRate = playbackRate * buffer.duration;
|
||||||
}
|
}
|
||||||
return playbackRate;
|
return { buffer, playbackRate };
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// creates playback ready AudioBufferSourceNode from hapValue
|
// creates playback ready AudioBufferSourceNode from hapValue
|
||||||
export function getSampleBufferSource(hapValue, buffer,transpose) {
|
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||||
|
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
|
||||||
if (hapValue.speed < 0) {
|
if (hapValue.speed < 0) {
|
||||||
// should this be cached?
|
// should this be cached?
|
||||||
buffer = reverseBuffer(buffer);
|
buffer = reverseBuffer(buffer);
|
||||||
}
|
}
|
||||||
const playbackRate = getBufferPlaybackRate(hapValue, buffer, transpose)
|
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const bufferSource = ac.createBufferSource();
|
const bufferSource = ac.createBufferSource();
|
||||||
bufferSource.buffer = buffer;
|
bufferSource.buffer = buffer;
|
||||||
@@ -120,20 +120,13 @@ function githubPath(base, subpath = '') {
|
|||||||
if (!base.startsWith('github:')) {
|
if (!base.startsWith('github:')) {
|
||||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
}
|
}
|
||||||
let path = base.slice('github:'.length);
|
let [_, path] = base.split('github:');
|
||||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
if (path.split('/').length === 2) {
|
||||||
let components = path.split('/');
|
// assume main as default branch if none set
|
||||||
let user = components[0];
|
path += '/main';
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
other = other.join('/');
|
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||||
|
|
||||||
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||||
@@ -263,7 +256,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||||||
|
|
||||||
const cutGroups = [];
|
const cutGroups = [];
|
||||||
|
|
||||||
export async function onTriggerSample(t, value, onended, bufferSrc) {
|
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||||
let {
|
let {
|
||||||
s,
|
s,
|
||||||
nudge = 0, // TODO: is this in seconds?
|
nudge = 0, // TODO: is this in seconds?
|
||||||
@@ -285,7 +278,7 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
|
|||||||
// destructure adsr here, because the default should be different for synths and samples
|
// 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]);
|
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||||
|
|
||||||
const { bufferSource, sliceDuration, offset } = bufferSrc
|
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
||||||
|
|
||||||
// asny stuff above took too long?
|
// asny stuff above took too long?
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
@@ -347,21 +340,14 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
|
|||||||
return handle;
|
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) {
|
function registerSample(key, bank, params) {
|
||||||
registerSound(key, async (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined)), {
|
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||||
type: 'sample',
|
type: 'sample',
|
||||||
samples: bank,
|
samples: bank,
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function registerSampleSource(key, bank, params) {
|
export function registerSampleSource(key, bank, params) {
|
||||||
const isWavetable = key.startsWith('wt_');
|
const isWavetable = key.startsWith('wt_');
|
||||||
if (isWavetable) {
|
if (isWavetable) {
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import './reverb.mjs';
|
|||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
@@ -155,7 +154,6 @@ let defaultDefaultValues = {
|
|||||||
phaserdepth: 0.75,
|
phaserdepth: 0.75,
|
||||||
shapevol: 1,
|
shapevol: 1,
|
||||||
distortvol: 1,
|
distortvol: 1,
|
||||||
distorttype: 0,
|
|
||||||
delay: 0,
|
delay: 0,
|
||||||
byteBeatExpression: '0',
|
byteBeatExpression: '0',
|
||||||
delayfeedback: 0.5,
|
delayfeedback: 0.5,
|
||||||
@@ -202,6 +200,25 @@ export function setVersionDefaults(version) {
|
|||||||
|
|
||||||
export const resetLoadedSounds = () => soundMap.set({});
|
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 = [];
|
let externalWorklets = [];
|
||||||
export function registerWorklet(url) {
|
export function registerWorklet(url) {
|
||||||
externalWorklets.push(url);
|
externalWorklets.push(url);
|
||||||
@@ -456,7 +473,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
shapevol = getDefaultValue('shapevol'),
|
shapevol = getDefaultValue('shapevol'),
|
||||||
distort,
|
distort,
|
||||||
distortvol = getDefaultValue('distortvol'),
|
distortvol = getDefaultValue('distortvol'),
|
||||||
distorttype = getDefaultValue('distorttype'),
|
|
||||||
pan,
|
pan,
|
||||||
vowel,
|
vowel,
|
||||||
delay = getDefaultValue('delay'),
|
delay = getDefaultValue('delay'),
|
||||||
@@ -630,7 +646,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// effects
|
// effects
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||||
|
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
||||||
|
|
||||||
if (tremolosync != null) {
|
if (tremolosync != null) {
|
||||||
tremolo = cps * tremolosync;
|
tremolo = cps * tremolosync;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { clamp } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
import { registerSound, soundMap } from './superdough.mjs';
|
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
destroyAudioWorkletNode,
|
destroyAudioWorkletNode,
|
||||||
|
|||||||
@@ -80,13 +80,12 @@ export function secondsToCycle(t, cps) {
|
|||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
export function getCommonSampleInfoFromBank(hapValue, bank) {
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
let midi = valueToMidi(hapValue, 36);
|
||||||
|
let transpose = midi - 36; // C3 is middle C;
|
||||||
let url;
|
let url;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let {transpose, label} = getCommonSampleInfo(hapValue)
|
|
||||||
|
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
url = bank[index];
|
url = bank[index];
|
||||||
@@ -103,20 +102,6 @@ export function getCommonSampleInfoFromBank(hapValue, bank) {
|
|||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
url = bank[closest][index];
|
||||||
}
|
}
|
||||||
label = `${s}:${index}`;
|
const label = `${s}:${index}`;
|
||||||
return { transpose, url, label };
|
return { transpose, url, index, midi, 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,7 +1,6 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getCommonSampleInfoFromBank } from './util.mjs';
|
import { getCommonSampleInfo } from './util.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
destroyAudioWorkletNode,
|
destroyAudioWorkletNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
@@ -216,7 +215,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
||||||
}
|
}
|
||||||
const frequency = getFrequencyFromValue(value);
|
const frequency = getFrequencyFromValue(value);
|
||||||
const { url, label } = getCommonSampleInfoFromBank(value, tables);
|
const { url, label } = getCommonSampleInfo(value, tables);
|
||||||
const payload = await getPayload(url, label, frameLen);
|
const payload = await getPayload(url, label, frameLen);
|
||||||
let holdEnd = t + duration;
|
let holdEnd = t + duration;
|
||||||
if (clip !== undefined) {
|
if (clip !== undefined) {
|
||||||
@@ -231,12 +230,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
begin: t,
|
begin: t,
|
||||||
end: envEnd,
|
end: envEnd,
|
||||||
frequency,
|
frequency,
|
||||||
freqspread: value.detune,
|
detune: value.detune,
|
||||||
position: value.wt,
|
position: value.wt,
|
||||||
warp: value.warp,
|
warp: value.warp,
|
||||||
warpMode: warpmode,
|
warpMode: warpmode,
|
||||||
voices: Math.max(value.unison ?? 1, 1),
|
voices: Math.max(value.unison ?? 1, 1),
|
||||||
panspread: value.spread,
|
spread: value.spread,
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||||
},
|
},
|
||||||
{ outputChannelCount: [2] },
|
{ outputChannelCount: [2] },
|
||||||
@@ -309,7 +308,6 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||||
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
|
||||||
const envGain = ac.createGain();
|
const envGain = ac.createGain();
|
||||||
const node = source.connect(envGain);
|
const node = source.connect(envGain);
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||||
@@ -320,7 +318,6 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(source);
|
destroyAudioWorkletNode(source);
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
fm?.stop();
|
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
wtPosModulators?.disconnect();
|
wtPosModulators?.disconnect();
|
||||||
wtWarpModulators?.disconnect();
|
wtWarpModulators?.disconnect();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
import OLAProcessor from './ola-processor';
|
import OLAProcessor from './ola-processor';
|
||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
const mod = (n, m) => ((n % m) + m) % m;
|
const mod = (n, m) => ((n % m) + m) % m;
|
||||||
@@ -431,10 +430,9 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ processorOptions }) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
process(inputs, outputs, parameters) {
|
||||||
@@ -446,12 +444,13 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.started = hasInput;
|
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 n = 0; n < blockSize; n++) {
|
||||||
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
for (let i = 0; i < input.length; i++) {
|
||||||
const shape = Math.expm1(pv(parameters.distort, n));
|
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
|
||||||
for (let ch = 0; ch < input.length; ch++) {
|
|
||||||
const x = input[ch][n];
|
|
||||||
output[ch][n] = postgain * this.algorithm(x, shape);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1050,15 +1049,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
|
||||||
{ name: 'detune', defaultValue: 0 },
|
{ name: 'detune', defaultValue: 0.18 },
|
||||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
|
||||||
{ name: 'warpMode', defaultValue: 0 },
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
{ name: 'voices', defaultValue: 1, min: 1 },
|
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
|
||||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
|
||||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1236,12 +1234,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
_sampleFrame(frame, phase) {
|
_sampleFrame(frame, phase) {
|
||||||
const len = frame.length;
|
const len = frame.length;
|
||||||
const pos = phase * len;
|
const pos = phase * len;
|
||||||
let i = pos | 0;
|
const i = pos | 0;
|
||||||
if (i >= len) i = 0; // fast wrap
|
|
||||||
const frac = pos - i;
|
const frac = pos - i;
|
||||||
const a = frame[i];
|
const a = frame[i];
|
||||||
let i1 = i + 1;
|
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
|
||||||
if (i1 >= len) i1 = 0;
|
|
||||||
const b = frame[i1];
|
const b = frame[i1];
|
||||||
return a + (b - a) * frac;
|
return a + (b - a) * frac;
|
||||||
}
|
}
|
||||||
@@ -1271,7 +1267,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
for (let i = 0; i < outL.length; i++) {
|
for (let i = 0; i < outL.length; i++) {
|
||||||
const detune = pv(parameters.detune, i);
|
const detune = pv(parameters.detune, i);
|
||||||
const freqspread = pv(parameters.freqspread, i);
|
|
||||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
const fIdx = idx | 0;
|
const fIdx = idx | 0;
|
||||||
@@ -1280,9 +1275,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const warpMode = pv(parameters.warpMode, i);
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
const voices = pv(parameters.voices, i);
|
const voices = pv(parameters.voices, i);
|
||||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
|
||||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
|
||||||
let f = pv(parameters.frequency, i);
|
let f = pv(parameters.frequency, i);
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||||
const normalizer = 1 / Math.sqrt(voices);
|
const normalizer = 1 / Math.sqrt(voices);
|
||||||
@@ -1295,7 +1290,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
gainL = gain2;
|
gainL = gain2;
|
||||||
gainR = gain1;
|
gainR = gain1;
|
||||||
}
|
}
|
||||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
|
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
|
||||||
const dPhase = fVoice * this.invSR;
|
const dPhase = fVoice * this.invSR;
|
||||||
const level = this._chooseMip(dPhase);
|
const level = this._chooseMip(dPhase);
|
||||||
const table = this.tables[level];
|
const table = this.tables[level];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//import { ZZFX } from 'zzfx';
|
//import { ZZFX } from 'zzfx';
|
||||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound } from './superdough.mjs';
|
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
|
||||||
import { buildSamples } from './zzfx_fork.mjs';
|
import { buildSamples } from './zzfx_fork.mjs';
|
||||||
|
|
||||||
export const getZZFX = (value, t) => {
|
export const getZZFX = (value, t) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './superdough.mjs';
|
||||||
|
|
||||||
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
||||||
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
||||||
|
|||||||
@@ -2829,82 +2829,6 @@ 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`] = `
|
exports[`runs examples > example "djf" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
|
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
|
||||||
@@ -10920,6 +10844,23 @@ exports[`runs examples > example "take" example index 2 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "timeline" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "timeline" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 4/7 | note:F3 ]",
|
||||||
|
"[ (4/7 → 1/1) ⇝ 8/7 | note:G3 ]",
|
||||||
|
"[ 4/7 ⇜ (1/1 → 8/7) | note:G3 ]",
|
||||||
|
"[ 8/7 → 12/7 | note:Ab3 ]",
|
||||||
|
"[ (12/7 → 2/1) ⇝ 16/7 | note:Bb3 ]",
|
||||||
|
"[ 12/7 ⇜ (2/1 → 16/7) | note:Bb3 ]",
|
||||||
|
"[ 16/7 → 20/7 | note:C4 ]",
|
||||||
|
"[ (20/7 → 3/1) ⇝ 24/7 | note:Db4 ]",
|
||||||
|
"[ 20/7 ⇜ (3/1 → 24/7) | note:Db4 ]",
|
||||||
|
"[ 24/7 → 4/1 | note:Eb4 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "tour" example index 0 1`] = `
|
exports[`runs examples > example "tour" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:e s:folkharp ]",
|
"[ 0/1 → 1/8 | note:e s:folkharp ]",
|
||||||
|
|||||||
@@ -60,23 +60,6 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
|
|||||||
.room(0.5)`}
|
.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
|
# Comments
|
||||||
|
|
||||||
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio';
|
import { registerSampleSource } from '@strudel/webaudio';
|
||||||
import { isAudioFile } from './files.mjs';
|
import { isAudioFile } from './files.mjs';
|
||||||
import { getSoundIndex, logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
import { registerSound } from '@strudel/webaudio';
|
|
||||||
import { getCommonSampleInfo } from '../../../packages/superdough/util.mjs';
|
|
||||||
|
|
||||||
//utilites for writing and reading to the indexdb
|
//utilites for writing and reading to the indexdb
|
||||||
|
|
||||||
@@ -27,15 +25,6 @@ function clearAllIDB() {
|
|||||||
|
|
||||||
export function clearIDB(dbName) {
|
export function clearIDB(dbName) {
|
||||||
return window.indexedDB.deleteDatabase(dbName);
|
return window.indexedDB.deleteDatabase(dbName);
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function registerSampleFromIdb(key, bank, params) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// queries the DB, and registers the sounds so they can be played
|
// queries the DB, and registers the sounds so they can be played
|
||||||
@@ -63,76 +52,31 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
if (!isAudioFile(title)) {
|
if (!isAudioFile(title)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitRelativePath = soundFile.id.split('/');
|
const splitRelativePath = soundFile.id.split('/');
|
||||||
let parentDirectory =
|
let parentDirectory =
|
||||||
//fallback to file name before period and seperator if no parent directory
|
//fallback to file name before period and seperator if no parent directory
|
||||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||||
|
const blob = soundFile.blob;
|
||||||
// const blob = soundFile.blob;
|
|
||||||
|
return blobToDataUrl(blob).then((soundPath) => {
|
||||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||||
|
|
||||||
titlePathMap.set(title, soundFile.id);
|
titlePathMap.set(title, soundPath);
|
||||||
|
|
||||||
sounds.set(parentDirectory, titlePathMap);
|
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(() => {
|
.then(() => {
|
||||||
sounds.forEach((titlePathMap, key) => {
|
sounds.forEach((titlePathMap, key) => {
|
||||||
const bank = Array.from(titlePathMap.keys())
|
const value = Array.from(titlePathMap.keys())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
})
|
})
|
||||||
.map((title) => titlePathMap.get(title));
|
.map((title) => titlePathMap.get(title));
|
||||||
|
|
||||||
|
registerSampleSource(key, value, { prebake: false });
|
||||||
|
|
||||||
|
|
||||||
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');
|
logger('imported sounds registered!', 'success');
|
||||||
|
|||||||
Reference in New Issue
Block a user