diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 7e3c2a8e..c8005927 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2040,6 +2040,7 @@ export const { real } = registerControl('real'); export const { imag } = registerControl('imag'); export const { enhance } = registerControl('enhance'); export const { partials } = registerControl('partials'); +export const { phases } = registerControl('phases'); export const { comb } = registerControl('comb'); export const { smear } = registerControl('smear'); export const { scram } = registerControl('scram'); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 6bd6fde6..b4dc825f 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -228,7 +228,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n); export const run = (n) => saw.range(0, n).round().segment(n); /** - * Creates a pattern from a binary number. + * Creates a binary pattern from a number. * * @name binary * @param {number} n - input number to convert to binary @@ -242,7 +242,7 @@ export const binary = (n) => { }; /** - * Creates a pattern from a binary number, padded to n bits long. + * Creates a binary pattern from a number, padded to n bits long. * * @name binaryN * @param {number} n - input number to convert to binary @@ -258,6 +258,40 @@ export const binaryN = (n, nBits = 16) => { return reify(n).segment(nBits).brshift(bitPos).band(pure(1)); }; +/** + * Creates a binary list pattern from a number. + * + * @name binaryL + * @param {number} n - input number to convert to binary + */ +export const binaryL = (n) => { + const nBits = reify(n).log2(0).floor().add(1); + return binaryNL(n, nBits); +}; + +/** + * Creates a binary list pattern from a number, padded to n bits long. + * + * @name binaryNL + * @param {number} n - input number to convert to binary + * @param {number} nBits - pattern length, defaults to 16 + */ +export const binaryNL = (n, nBits = 16) => { + return reify(n) + .withValue((v) => (bits) => { + const bList = []; + for (let i = bits - 1; i >= 0; i--) { + bList.push((v >> i) & 1); + } + return bList; + }) + .appLeft(reify(nBits)); +}; + +export const randL = (n) => { + return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n)); +}; + export const randrun = (n) => { return signal((t) => { // Without adding 0.5, the first cycle is always 0,1,2,3,... diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c877..625f4075 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - it('Can apply a function', () => { + (it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - }); + })); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 5dc5dc08..71c4a15d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -14,9 +14,10 @@ import { noises, webAudioTimeout, } from './helpers.mjs'; +import { logger } from './logger.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; +const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user']; const waveformAliases = [ ['tri', 'triangle'], ['sqr', 'square'], @@ -413,9 +414,13 @@ export function registerSynthSounds() { waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); } -export function waveformN(partials, type) { - const real = new Float32Array(partials + 1); - const imag = new Float32Array(partials + 1); +const PI2 = 2 * Math.PI; +export function waveformN(partials, phases, type) { + const isList = typeof partials === 'object'; + partials = isList ? partials : new Float32Array(partials).fill(1); + const len = partials.length; + const real = new Float32Array(len + 1); + const imag = new Float32Array(len + 1); const ac = getAudioContext(); const osc = ac.createOscillator(); @@ -423,20 +428,29 @@ export function waveformN(partials, type) { sawtooth: (n) => [0, -1 / n], square: (n) => [0, n % 2 === 0 ? 0 : 1 / n], triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0], + user: (_n) => [0, 1], }; if (!terms[type]) { throw new Error(`unknown wave type ${type}`); } - real[0] = 0; // dc offset - imag[0] = 0; - let n = 1; - while (n <= partials) { - const [r, i] = terms[type](n); - real[n] = r; - imag[n] = i; - n++; + for (let n = 0; n < len; n++) { + const mag = partials[n]; + const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset + const phase = phases?.[n] ?? 0; + // Scale by `partials` + let R = r * mag; + let I = i * mag; + // Apply rotation by the phase + if (phase !== 0) { + const c = Math.cos(PI2 * phase); + const s = Math.sin(PI2 * phase); + R = c * R - s * I; + I = s * R + c * I; + } + real[n + 1] = R; + imag[n + 1] = I; } const wave = ac.createPeriodicWave(real, imag); @@ -446,16 +460,24 @@ export function waveformN(partials, type) { // expects one of waveforms as s export function getOscillator(s, t, value) { - let { n: partials, duration, noise = 0 } = value; + const { duration, noise = 0 } = value; + const partials = value.partials ?? value.n; let o; + if (s === 'user' && !partials) { + logger( + `[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`, + ); + s = 'triangle'; + } + s = s === 'user' && !partials ? 'triangle' : s; // If no partials are given, use stock waveforms - if (!partials || s === 'sine') { + if (!partials || !partials?.length || s === 'sine') { o = getAudioContext().createOscillator(); o.type = s || 'triangle'; } // generate custom waveform if partials are given else { - o = waveformN(partials, s); + o = waveformN(partials, value.phases, s); } // set frequency o.frequency.value = getFrequencyFromValue(value); diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index 456ef9f6..b14146cb 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), - 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: + inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index eb79cccf..201d57df 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub -{' '} + ### speed