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