diff --git a/packages/core/bench/signal.bench.mjs b/packages/core/bench/signal.bench.mjs index c79cedc05..d92840ef6 100644 --- a/packages/core/bench/signal.bench.mjs +++ b/packages/core/bench/signal.bench.mjs @@ -2,45 +2,48 @@ import { describe, bench } from 'vitest'; import { calculateSteps, rand, useOldRandom } from '../index.mjs'; -const testingResolution = 1024; +const testingResolution = 128; const _generateRandomPattern = () => rand.iter(testingResolution).fast(testingResolution).firstCycle(); -describe('random', () => { - calculateSteps(true); - bench( - '+tactus', - _generateRandomPattern, - { time: 1000 }, - ); - - calculateSteps(false); - bench( - '-tactus', - _generateRandomPattern, - { time: 1000 }, - ); -}); - describe('old random', () => { calculateSteps(true); bench( '+tactus', () => { - useOldRandom(); - _generateRandomPattern(); + useOldRandom(); + _generateRandomPattern(); + }, + { + time: 1000, + teardown() { + useOldRandom(false); + }, }, - { time: 1000 }, ); calculateSteps(false); bench( '-tactus', () => { - useOldRandom(); - _generateRandomPattern(); + useOldRandom(); + _generateRandomPattern(); + }, + { + time: 1000, + teardown() { + useOldRandom(false); + }, }, - { time: 1000 }, ); }); + +describe('random', () => { + calculateSteps(true); + bench('+tactus', _generateRandomPattern, { time: 1000 }); + + calculateSteps(false); + bench('-tactus', _generateRandomPattern, { time: 1000 }); +}); + calculateSteps(true); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 78755bd98..492f6a7f0 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -201,9 +201,13 @@ function _murmurHashFinalizer(x) { return x >>> 0; // unsigned } -// Used to decorrelate nearby t and i prior to hashing -function _decorrelate(t, i = 0) { - const T = Math.floor(t * 536870912); // set 2^29 resolution +// Convert t to a 32 bit integer, preserving temporal resolution down to 1/2^29 +function _tToT(t) { + return Math.floor(t * 536870912); +} + +// Used to decorrelate nearby T and i prior to hashing +function _decorrelate(T, i = 0) { const lowBits = (T >>> 0) >>> 0; const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32 let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35); @@ -211,8 +215,19 @@ function _decorrelate(t, i = 0) { return key >>> 0; } -function randAt(t, i = 0) { - return _murmurHashFinalizer(_decorrelate(t, i)) / 4294967296; // 2^32 +function randAt(T, i = 0) { + return _murmurHashFinalizer(_decorrelate(T, i)) / 4294967296; // 2^32 +} + +// n samples at time t +function timeToRands(t, n) { + const T = _tToT(t); + if (n === 1) { + return randAt(T, 0); + } + const out = new Array(n); + for (let i = 0; i < n; i++) out[i] = randAt(T, i); + return out; } // Deprecated: Old random signals. Configuration `useOldRandom` may be used for legacy songs @@ -226,8 +241,10 @@ const __xorwise = (x) => { const __frac = (x) => x - Math.trunc(x); const __timeToIntSeed = (x) => __xorwise(Math.trunc(__frac(x / 300) * 536870912)); const __intSeedToRand = (x) => (x % 536870912) / 536870912; -const __timeToRand = (x) => Math.abs(__intSeedToRand(__timeToIntSeed(x))); const __timeToRandsPrime = (seed, n) => { + if (n === 1) { + return Math.abs(__intSeedToRand(seed)); + } const result = []; for (let i = 0; i < n; i++) { result.push(__intSeedToRand(seed)); @@ -240,6 +257,10 @@ const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n); // End old random let useOldRandomBool = false; +const getRandsAtTime = (t, n = 1) => { + return useOldRandomBool ? __timeToRands(t, n) : timeToRands(t, n); +}; + /** * Whether to use the old random number generator or not. Can be used to support legacy projects * @@ -248,28 +269,10 @@ let useOldRandomBool = false; * @example * useOldRandom(true) * // Repeats every 300 cycles - * $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32)._punchcard() - * $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32)._punchcard() + * $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32) + * $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32) */ -export const useOldRandom = (b = true) => useOldRandomBool = b; - -// N samples at time t -function timeToRands(t, n) { - if (useOldRandomBool) { - return __timeToRands(t, n); - } - const out = new Array(n); - for (let i = 0; i < n; i++) out[i] = randAt(t, i); - return out; -} - -// Single sample at time t -function timeToRand(t) { - if (useOldRandomBool) { - return __timeToRand(t); - } - return randAt(t, 0); -} +export const useOldRandom = (b = true) => (useOldRandomBool = b); /** * A discrete pattern of numbers from 0 to n-1 @@ -313,7 +316,7 @@ export const binaryN = (n, nBits = 16) => { export const randrun = (n) => { return signal((t) => { // Without adding 0.5, the first cycle is always 0,1,2,3,... - const rands = timeToRands(t.floor().add(0.5), n); + const rands = getRandsAtTime(t.floor().add(0.5), n); const nums = rands .map((n, i) => [n, i]) .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) @@ -363,7 +366,7 @@ export const scramble = register('scramble', (n, pat) => { * s("bd*4,hh*8").cutoff(rand.range(500,8000)) * */ -export const rand = signal(timeToRand); +export const rand = signal(getRandsAtTime); /** * A continuous pattern of random numbers, between -1 and 1 */ @@ -545,7 +548,7 @@ function _perlin(t) { let tb = ta + 1; const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3; const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a); - const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb)); + const v = interp(t - ta)(getRandsAtTime(ta))(getRandsAtTime(tb)); return v; } export const perlinWith = (tpat) => { @@ -556,8 +559,8 @@ function _berlin(t) { const prevRidgeStartIndex = Math.floor(t); const nextRidgeStartIndex = prevRidgeStartIndex + 1; - const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex); - const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint; + const prevRidgeBottomPoint = getRandsAtTime(prevRidgeStartIndex); + const nextRidgeTopPoint = getRandsAtTime(nextRidgeStartIndex) + prevRidgeBottomPoint; const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex); const interp = (a, b, t) => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index eb1466d91..9b6b20746 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -19,6 +19,17 @@ const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; let maxPolyphony = DEFAULT_MAX_POLYPHONY; +/** + * Set the max polyphony. If notes are ringing out via `release` then they will + * start to die out in first-in-first-out order once the max polyphony has been hit + * + * @name setMaxPolyphony + * @param {number} Max polyphony. Defaults to 128 + * @example + * setMaxPolyphony(4) + * n(irand(24).seg(8)).scale("C#3:minor").room(1).release(4).gain(0.5) + * + */ export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; } @@ -48,6 +59,17 @@ export function applyGainCurve(val) { return gainCurveFunc(val); } +/** + * Apply a function to all gains provided in patterns. Can be used to rescale gain to be + * quadratic, exponential, etc. rather than linear + * + * @name setGainCurve + * @param {Function} function to apply to all gain values + * @example + * setGainCurve((x) => x * x) // quadratic gain + * s("bd*4").gain(0.5) // equivalent to 0.25 gain normally + * + */ export function setGainCurve(newGainCurveFunc) { gainCurveFunc = newGainCurveFunc; } diff --git a/vitest.config.mjs b/vitest.config.mjs index dcdde1133..392b17310 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -7,7 +7,7 @@ export default defineConfig({ test: { reporters: 'verbose', isolate: false, - silent: true, + silent: false, exclude: [ '**/node_modules/**', '**/dist/**', @@ -16,5 +16,6 @@ export default defineConfig({ '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress}.config.*', '**/shared.test.mjs', ], + setupFiles: './vitest.setup.mjs', }, }); diff --git a/vitest.setup.mjs b/vitest.setup.mjs new file mode 100644 index 000000000..bb9d24b07 --- /dev/null +++ b/vitest.setup.mjs @@ -0,0 +1,7 @@ +import { afterEach } from 'vitest'; +import { useOldRandom } from './packages/core/signal.mjs'; + +afterEach(() => { + // Avoid bleed between tests + useOldRandom(false); +});