diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 791558339..9a2be7f44 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2264,7 +2264,6 @@ export const { tsdelay } = registerControl('tsdelay'); export const { real } = registerControl('real'); export const { imag } = registerControl('imag'); export const { enhance } = registerControl('enhance'); -export const { partials } = registerControl('partials'); export const { comb } = registerControl('comb'); export const { smear } = registerControl('smear'); export const { scram } = registerControl('scram'); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index e29769900..4737f0db0 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3624,3 +3624,66 @@ for (const name of distAlgoNames) { return this.distort(argsPat); }; } + +/** + * Turns a list of patterns into a single pattern which outputs list-values + * + * @name parray + * @returns Pattern + */ +export const parray = (pats) => { + const pack = (...xs) => xs; + let acc = pure(curry(pack, null, pats.length)); + for (const p of pats) acc = acc.appBoth(reify(p)); + return acc; +}; + +const _ensureListPattern = (list) => { + if (Array.isArray(list)) { + return parray(list); + } + return reify(list); +}; + +/** + * Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..) + * + * Can also be used to create a new synth via `s('user').partials(...)` + * + * @name partials + * @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped) + * @example + * s("user").seg(16).n(irand(8)).scale("A:major") + * .partials([1, 0, 1, 0, 0, 1]) + * @example + * s("saw").seg(8).n(irand(12)).scale("G#:minor") + * .partials(binaryL(irand(256).add("1"))) + */ +Pattern.prototype.partials = function (list) { + return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list)); +}; + +// Also create a top-level function +export const partials = (list) => { + return _ensureListPattern(list).as('partials'); +}; + +/** + * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases + * + * @name phases + * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped) + * @example + * // Phase cancellation + * s("saw").seg(8).n(irand(12)).scale("G#1:minor") + * .partials(partials([1, 1, 1])) + * .superimpose(x => x.phases([0.5, 0.5, 0.5])) + */ +Pattern.prototype.phases = function (list) { + return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list)); +}; + +// Also create a top-level function +export const phases = (list) => { + return _ensureListPattern(list).as('phases'); +}; diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 6bd6fde67..604d1c805 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,51 @@ 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 + * s("saw").seg(8) + * .partials(binaryL(irand(4096).add(1))) + */ +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)); +}; + +/** + * Creates a list of random numbers of the given length + * + * @name randL + * @param {number} n Number of random numbers to sample + * @example + * s("saw").seg(16).n(irand(12)).scale("F1:minor") + * .partials(randL(8)) + */ +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/superdough/synth.mjs b/packages/superdough/synth.mjs index e35b98806..10df1776b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -15,9 +15,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'], @@ -414,9 +415,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(); @@ -424,20 +429,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); @@ -447,16 +461,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 === 0 || 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/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index aa251d601..32e61c31d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7219,6 +7219,112 @@ exports[`runs examples > example "panchor" example index 0 1`] = ` ] `; +exports[`runs examples > example "partials" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 1/16 → 1/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 1/8 → 3/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 3/16 → 1/4 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 1/4 → 5/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 5/16 → 3/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 3/8 → 7/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 7/16 → 1/2 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 1/2 → 9/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 9/16 → 5/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 5/8 → 11/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 11/16 → 3/4 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 3/4 → 13/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 13/16 → 7/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 7/8 → 15/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 15/16 → 1/1 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 1/1 → 17/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 17/16 → 9/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 9/8 → 19/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 19/16 → 5/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 5/4 → 21/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 21/16 → 11/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 11/8 → 23/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 23/16 → 3/2 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 3/2 → 25/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 25/16 → 13/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 13/8 → 27/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 27/16 → 7/4 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 7/4 → 29/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 29/16 → 15/8 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 15/8 → 31/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 31/16 → 2/1 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 2/1 → 33/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 33/16 → 17/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 17/8 → 35/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 35/16 → 9/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 9/4 → 37/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 37/16 → 19/8 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 19/8 → 39/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 39/16 → 5/2 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 5/2 → 41/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 41/16 → 21/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 21/8 → 43/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 43/16 → 11/4 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 11/4 → 45/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 45/16 → 23/8 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 23/8 → 47/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 47/16 → 3/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 3/1 → 49/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 49/16 → 25/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 25/8 → 51/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 51/16 → 13/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 13/4 → 53/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 53/16 → 27/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 27/8 → 55/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 55/16 → 7/2 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 7/2 → 57/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 57/16 → 29/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 29/8 → 59/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 59/16 → 15/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 15/4 → 61/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 61/16 → 31/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 31/8 → 63/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 63/16 → 4/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", +] +`; + +exports[`runs examples > example "partials" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw note:G#3 partials:[1] ]", + "[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 1 1 0 0 0 0] ]", + "[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 1 1 1 1 1] ]", + "[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 1 0 0 1 1 1] ]", + "[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 1 1] ]", + "[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 1 1] ]", + "[ 3/4 → 7/8 | s:saw note:B3 partials:[1 1 0 0 1 1] ]", + "[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 1 0 0 1 1 0] ]", + "[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 1 1 0] ]", + "[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 1 0 1 1 0 1] ]", + "[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 1 1 1 0 1 1] ]", + "[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 1 1 1 1] ]", + "[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 1 1 1 0 0] ]", + "[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0] ]", + "[ 7/4 → 15/8 | s:saw note:B3 partials:[1 1 0 1 0 1] ]", + "[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 1 1 0 0 1] ]", + "[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 1 1 1 0 1 1 0] ]", + "[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 1 1 0 1 0 0 0] ]", + "[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 1 1 0 0 1] ]", + "[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 1 0 1 0 1 1 1] ]", + "[ 5/2 → 21/8 | s:saw note:E4 partials:[1 1 1 0 1 1 0] ]", + "[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 1 0 1 0] ]", + "[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 1 0 0 0 1 1] ]", + "[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 1] ]", + "[ 3/1 → 25/8 | s:saw note:B3 partials:[1 1 1 0 0 0] ]", + "[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 1 1 0 0 0] ]", + "[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 1 1 1 1 1 1 1] ]", + "[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]", + "[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]", + "[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 1 1 1 0 0 0 1] ]", + "[ 15/4 → 31/8 | s:saw note:B4 partials:[1 1 0 1 0 0 0 0] ]", + "[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 1 1 1 1 0 1] ]", +] +`; + exports[`runs examples > example "pattack" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:c pattack:0 ]", @@ -7472,6 +7578,75 @@ exports[`runs examples > example "phasersweep" example index 0 1`] = ` ] `; +exports[`runs examples > example "phases" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} ]", + "[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} ]", + "[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} ]", + "[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} ]", + "[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} ]", + "[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} ]", + "[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} ]", + "[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} ]", + "[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} ]", + "[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} ]", + "[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} ]", + "[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} ]", + "[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} ]", + "[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} ]", + "[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} ]", + "[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} ]", + "[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} ]", + "[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} ]", + "[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} ]", + "[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} ]", + "[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} ]", + "[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} ]", + "[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} ]", + "[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} ]", + "[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} ]", + "[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} ]", + "[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", +] +`; + exports[`runs examples > example "pianoroll" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", @@ -8193,6 +8368,75 @@ exports[`runs examples > example "rand" example index 0 1`] = ` ] `; +exports[`runs examples > example "randL" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:saw note:F1 partials:[0 0 0 0 0 0 0 0] ]", + "[ 1/16 → 1/8 | s:saw note:Bb2 partials:[0.8426077850162983 0.0886186733841896 0.9342893119901419 0.06848422810435295 0.6248505394905806 0.5372848063707352 0.8283301629126072 0.7969411127269268] ]", + "[ 1/8 → 3/16 | s:saw note:G2 partials:[0.6852155700325966 0.17723728902637959 0.8521428182721138 0.5022399611771107 0.9780306946486235 0.3186125475913286 0.1851645242422819 0.8495976086705923] ]", + "[ 3/16 → 1/4 | s:saw note:Ab1 partials:[0.20066574029624462 0.17195586115121841 0.2159541044384241 0.17005567252635956 0.6841662060469389 0.07906394638121128 0.004815371707081795 0.027107616886496544] ]", + "[ 1/4 → 5/16 | s:saw note:C2 partials:[0.36975969187915325 0.18273563869297504 0.012781793251633644 0.5423613861203194 0.12467948533594608 0.7188410349190235 0.856887087225914 0.8818203993141651] ]", + "[ 5/16 → 3/8 | s:saw note:Eb2 partials:[0.5675661638379097 0.15932588279247284 0.31376867927610874 0.32359412126243114 0.3281281068921089 0.011714376509189606 0.4433113746345043 0.68459103256464] ]", + "[ 3/8 → 7/16 | s:saw note:C2 partials:[0.40139251574873924 0.15799915604293346 0.9604704882949591 0.03873417526483536 0.4157217647880316 0.27756719291210175 0.8800551909953356 0.5692523363977671] ]", + "[ 7/16 → 1/2 | s:saw note:Bb1 partials:[0.3015887886285782 0.22311350144445896 0.47645803540945053 0.001545613631606102 0.53841289319098 0.330710094422102 0.2747743520885706 0.9471044968813658] ]", + "[ 1/2 → 9/16 | s:saw note:Bb1 partials:[0.2604806162416935 0.3654713351279497 0.9900747090578079 0.22572625242173672 0.9328999016433954 0.5273839123547077 0.7047769222408533 0.7311522178351879] ]", + "[ 9/16 → 5/8 | s:saw note:G1 partials:[0.16399178467690945 0.9559339117258787 0.21830322034657001 0.4903192054480314 0.2504696864634752 0.7302105724811554 0.6580934692174196 0.9855979979038239] ]", + "[ 5/8 → 11/16 | s:saw note:G1 partials:[0.1356358677148819 0.5403102282434702 0.07321739941835403 0.9646544624119997 0.7707359045743942 0.46888123638927937 0.9489036612212658 0.366748945787549] ]", + "[ 11/16 → 3/4 | s:saw note:C2 partials:[0.408811716362834 0.9613851886242628 0.7683626655489206 0.5862949229776859 0.9568102769553661 0.7154356613755226 0.6283384170383215 0.2193678840994835] ]", + "[ 3/4 → 13/16 | s:saw note:Ab1 partials:[0.19582648016512394 0.0024610888212919235 0.08272589556872845 0.09592138417065144 0.9678201265633106 0.3249557167291641 0.9463537875562906 0.20766610652208328] ]", + "[ 13/16 → 7/8 | s:saw note:Ab2 partials:[0.7516226731240749 0.15610851533710957 0.9039078876376152 0.8602268267422915 0.928601048886776 0.4479671400040388 0.5080656576901674 0.562442347407341] ]", + "[ 7/8 → 15/16 | s:saw note:C2 partials:[0.3976310808211565 0.13476407527923584 0.963376859202981 0.6579397786408663 0.5203975513577461 0.865439185872674 0.5583905670791864 0.2736878804862499] ]", + "[ 15/16 → 1/1 | s:saw note:G2 partials:[0.7029578909277916 0.6351367030292749 0.26143294386565685 0.6411824338138103 0.5676082745194435 0.4803342465311289 0.5140306428074837 0.9477859679609537] ]", + "[ 1/1 → 17/16 | s:saw note:Eb2 partials:[0.5195421651005745 0.9349692352116108 0.1718774326145649 0.9601866770535707 0.2802200373262167 0.8732441551983356 0.01810968853533268 0.42737805284559727] ]", + "[ 17/16 → 9/8 | s:saw note:Eb2 partials:[0.5486328881233931 0.7088070474565029 0.07877279818058014 0.9422610364854336 0.7696988433599472 0.8289324026554823 0.5925844628363848 0.3920764606446028] ]", + "[ 9/8 → 19/16 | s:saw note:G2 partials:[0.6724895145744085 0.21410975232720375 0.5229047331959009 0.20957534946501255 0.7402758821845055 0.2681974694132805 0.4059371296316385 0.9422509074211121] ]", + "[ 19/16 → 5/4 | s:saw note:F1 partials:[0.08174665085971355 0.2762963864952326 0.5428560189902782 0.29452037811279297 0.17946280725300312 0.366960596293211 0.26327736489474773 0.5002023074775934] ]", + "[ 5/4 → 21/16 | s:saw note:G2 partials:[0.7287282031029463 0.9341024849563837 0.5098182689398527 0.5608645845204592 0.6812942810356617 0.5015129633247852 0.4602120481431484 0.927369873970747] ]", + "[ 21/16 → 11/8 | s:saw note:F1 partials:[0.08104278706014156 0.5200720727443695 0.5363391060382128 0.9824271816760302 0.10652091167867184 0.23473932035267353 0.7656102329492569 0.4850195776671171] ]", + "[ 11/8 → 23/16 | s:saw note:Ab1 partials:[0.18280375190079212 0.40527783520519733 0.30504362285137177 0.7622128054499626 0.842598931863904 0.053796686232089996 0.5124214049428701 0.9178454764187336] ]", + "[ 23/16 → 3/2 | s:saw note:Eb2 partials:[0.5081270858645439 0.1421387754380703 0.8852288406342268 0.9066353775560856 0.7700740322470665 0.35437702015042305 0.20032598450779915 0.963155921548605] ]", + "[ 3/2 → 25/16 | s:saw note:F2 partials:[0.6084080748260021 0.4424846563488245 0.755185678601265 0.04328125901520252 0.04244415462017059 0.3994515985250473 0.787989066913724 0.3180440980941057] ]", + "[ 25/16 → 13/8 | s:saw note:Ab1 partials:[0.17092766426503658 0.7808954659849405 0.8234915658831596 0.8643151633441448 0.9781719036400318 0.5102015696465969 0.018259897828102112 0.25844234600663185] ]", + "[ 13/8 → 27/16 | s:saw note:Db2 partials:[0.49675471149384975 0.2965749856084585 0.09848833456635475 0.19172007404267788 0.22677889838814735 0.9841996673494577 0.11156083643436432 0.6079028844833374] ]", + "[ 27/16 → 7/4 | s:saw note:Bb1 partials:[0.29589061066508293 0.13011129200458527 0.8021425940096378 0.38040103763341904 0.33274981752038 0.29745868407189846 0.9898250997066498 0.743482418358326] ]", + "[ 7/4 → 29/16 | s:saw note:Ab1 partials:[0.20473789609968662 0.7480021081864834 0.30757393687963486 0.5827204789966345 0.22094514779746532 0.6370698790997267 0.16573002003133297 0.0638319905847311] ]", + "[ 29/16 → 15/8 | s:saw note:Db2 partials:[0.4695742893964052 0.6690364442765713 0.6216684486716986 0.41342394426465034 0.9508822970092297 0.8403679896146059 0.91759910620749 0.6591395419090986] ]", + "[ 15/8 → 31/16 | s:saw note:F2 partials:[0.5945571791380644 0.3962489552795887 0.563431927934289 0.4742323160171509 0.4185752831399441 0.9703940469771624 0.5282467231154442 0.6896266285330057] ]", + "[ 31/16 → 2/1 | s:saw note:Bb2 partials:[0.8672592639923096 0.3438429981470108 0.9795673936605453 0.46547594852745533 0.9770395755767822 0.36851615831255913 0.7725539114326239 0.876962523907423] ]", + "[ 2/1 → 33/16 | s:saw note:C3 partials:[0.9595271199941635 0.5427457969635725 0.45864437520504 0.8057199101895094 0.18388565629720688 0.17221237532794476 0.8838487900793552 0.4976818822324276] ]", + "[ 33/16 → 17/8 | s:saw note:Bb1 partials:[0.3324784208089113 0.6005446836352348 0.1564352661371231 0.765217661857605 0.12899602390825748 0.20880493707954884 0.9156261626631021 0.5550615340471268] ]", + "[ 17/8 → 35/16 | s:saw note:Bb2 partials:[0.9033902939409018 0.22919894196093082 0.8388008493930101 0.9108077362179756 0.32246968522667885 0.16122018359601498 0.31932324543595314 0.18524732999503613] ]", + "[ 35/16 → 9/4 | s:saw note:F1 partials:[0.037000780925154686 0.30360451713204384 0.7516817897558212 0.17065968923270702 0.24669718369841576 0.978821286931634 0.9620356522500515 0.12417634017765522] ]", + "[ 9/4 → 37/16 | s:saw note:C2 partials:[0.34548251144587994 0.30419893004000187 0.10205957666039467 0.9906709585338831 0.3156223688274622 0.3745057098567486 0.7668170686811209 0.9379972033202648] ]", + "[ 37/16 → 19/8 | s:saw note:F2 partials:[0.6649543270468712 0.41613837145268917 0.9078915324062109 0.2232209537178278 0.2918607946485281 0.4931973237544298 0.7035325299948454 0.29280414804816246] ]", + "[ 19/8 → 39/16 | s:saw note:Bb2 partials:[0.8365066405385733 0.5369812995195389 0.2263860274106264 0.21759207546710968 0.2172116208821535 0.09454222768545151 0.8075542915612459 0.9017798062413931] ]", + "[ 39/16 → 5/2 | s:saw note:G1 partials:[0.14231421053409576 0.6801821701228619 0.7958894111216068 0.9494249671697617 0.179213372990489 0.22531690262258053 0.7458387855440378 0.3595987129956484] ]", + "[ 5/2 → 41/16 | s:saw note:Db2 partials:[0.45861607417464256 0.6807645913213491 0.07009582966566086 0.1450389064848423 0.2579921595752239 0.612881500273943 0.5503941811621189 0.7027011960744858] ]", + "[ 41/16 → 21/8 | s:saw note:F2 partials:[0.6068673655390739 0.8489279896020889 0.027898555621504784 0.90520747192204 0.18605150654911995 0.30939464271068573 0.16843407973647118 0.5127317048609257] ]", + "[ 21/8 → 43/16 | s:saw note:G1 partials:[0.16019348427653313 0.47758268006145954 0.9749126061797142 0.8025561925023794 0.3600110989063978 0.324309878051281 0.5655391272157431 0.13364790193736553] ]", + "[ 43/16 → 11/4 | s:saw note:Eb2 partials:[0.538035349920392 0.4103843830525875 0.6443832442164421 0.7532152868807316 0.32128027081489563 0.29064710810780525 0.5972099266946316 0.8252892810851336] ]", + "[ 11/4 → 45/16 | s:saw note:F2 partials:[0.6332327704876661 0.7342763151973486 0.16935866326093674 0.4754706546664238 0.528122790157795 0.21934260986745358 0.7476693484932184 0.2995396424084902] ]", + "[ 45/16 → 23/8 | s:saw note:Bb2 partials:[0.8912940509617329 0.5256196465343237 0.25639201514422894 0.46860250271856785 0.08611978217959404 0.6175916157662868 0.990638293325901 0.11189854890108109] ]", + "[ 23/8 → 47/16 | s:saw note:F1 partials:[0.01625417172908783 0.28409438766539097 0.3075305689126253 0.40343056432902813 0.5556836389005184 0.6926821582019329 0.5635769125074148 0.017873913049697876] ]", + "[ 47/16 → 3/1 | s:saw note:Bb1 partials:[0.2830589488148689 0.3613663297146559 0.6216250211000443 0.6083405166864395 0.27202711440622807 0.771099541336298 0.3156145606189966 0.736228458583355] ]", + "[ 3/1 → 49/16 | s:saw note:Ab1 partials:[0.21728911064565182 0.7605195846408606 0.11240843310952187 0.4372697602957487 0.15630115941166878 0.1653979979455471 0.5303416550159454 0.07218753732740879] ]", + "[ 49/16 → 25/8 | s:saw note:F1 partials:[0.012518879026174545 0.6565517522394657 0.783835593611002 0.2481316588819027 0.27786846831440926 0.6153149046003819 0.39914070069789886 0.2175555843859911] ]", + "[ 25/8 → 51/16 | s:saw note:C2 partials:[0.34324387833476067 0.14098095521330833 0.012934491038322449 0.827436288818717 0.5283997394144535 0.8942463714629412 0.4924008697271347 0.9298017006367445] ]", + "[ 51/16 → 13/4 | s:saw note:Bb2 partials:[0.9057559575885534 0.5731389932334423 0.06561482697725296 0.02156665176153183 0.4685337021946907 0.000788738951086998 0.6995994579046965 0.9893404543399811] ]", + "[ 13/4 → 53/16 | s:saw note:C3 partials:[0.9923650715500116 0.7797339502722025 0.1992435697466135 0.9005183521658182 0.718992218375206 0.23745290748775005 0.28872333094477654 0.35448253713548183] ]", + "[ 53/16 → 27/8 | s:saw note:Bb1 partials:[0.3235324025154114 0.5165992900729179 0.6231792941689491 0.8034896682947874 0.6663950439542532 0.4207208137959242 0.9748435858637094 0.27676148526370525] ]", + "[ 27/8 → 55/16 | s:saw note:C2 partials:[0.40869180113077164 0.4018078800290823 0.34696186147630215 0.9572948440909386 0.400035472586751 0.3531211093068123 0.7240961641073227 0.8125612027943134] ]", + "[ 55/16 → 7/2 | s:saw note:Ab2 partials:[0.775677714496851 0.0502743236720562 0.3246541116386652 0.503994770348072 0.8252716399729252 0.89872563816607 0.060319963842630386 0.25497629307210445] ]", + "[ 7/2 → 57/16 | s:saw note:C2 partials:[0.40994881466031075 0.6455810181796551 0.35704658553004265 0.2732649203389883 0.31587288342416286 0.23243548162281513 0.22569947130978107 0.17241661623120308] ]", + "[ 57/16 → 29/8 | s:saw note:Ab2 partials:[0.8133465722203255 0.8327706772834063 0.32914630882442 0.23131784796714783 0.7526696771383286 0.1361286472529173 0.11441296897828579 0.36366880126297474] ]", + "[ 29/8 → 59/16 | s:saw note:C3 partials:[0.9391485787928104 0.3380728308111429 0.7723016366362572 0.998674126341939 0.7193406894803047 0.6963680125772953 0.9260678570717573 0.7829763628542423] ]", + "[ 59/16 → 15/4 | s:saw note:C3 partials:[0.9731374885886908 0.0494034755975008 0.990720022469759 0.9819309785962105 0.22877203114330769 0.7265191469341516 0.4922411348670721 0.7549834884703159] ]", + "[ 15/4 → 61/16 | s:saw note:Ab2 partials:[0.8121673222631216 0.25548945367336273 0.5759696904569864 0.6668333616107702 0.9233786761760712 0.11143362522125244 0.9734930321574211 0.369325939565897] ]", + "[ 61/16 → 31/8 | s:saw note:G1 partials:[0.1008925698697567 0.7558664139360189 0.1523460578173399 0.6523381881415844 0.9690635204315186 0.7343240566551685 0.9544064309448004 0.9150135722011328] ]", + "[ 31/8 → 63/16 | s:saw note:G2 partials:[0.7361665386706591 0.7657648548483849 0.21474426984786987 0.8228576295077801 0.5647660344839096 0.08148551359772682 0.9714624118059874 0.13992334716022015] ]", + "[ 63/16 → 4/1 | s:saw note:Bb1 partials:[0.3056422360241413 0.8881660811603069 0.7683485243469477 0.08641545102000237 0.5427133180201054 0.20803124643862247 0.5347504671663046 0.5977730695158243] ]", +] +`; + exports[`runs examples > example "range" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh cutoff:2250 ]", diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 0fcc41363..56b28c0e4 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -48,28 +48,94 @@ You can also use the `crackle` type to play some subtle noise crackles. You can ### Additive Synthesis -To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform: +Periodic waveforms are composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality. + +For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions. + +#### Partials + +`partials` refers to the magnitude of each harmonic relative to the fundamental frequency. They can thus be used to spectrally filter these waveforms and tame some of their harshness: >".fast(2)) .sound("sawtooth") -.n("<32 16 8 4>") +.partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"]) ._scope()`} /> -When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. -You can also set `n` directly in mini notation with `sound`: +`partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source: >".fast(2)) -.sound("sawtooth:<32 16 8 4>") +.sound("user") +.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3]) ._scope()`} /> -Note for tidal users: `n` in tidal is synonymous to `note` for synths only. -In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms. +We may algorithmically construct lists of magnitudes with Javascript code like: + +>".fast(2)) +.sound("saw") +.partials(new Array(numHarmonics).fill(1)) +._scope()`} +/> + +which acts as a spectral filter. Or: + +>").fast(2) +.sound("user") +.partials(new Array(50).fill(0) + .map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1)) +) +._scope()`} +/> + +which recovers a familiar waveform. + +`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`: + +>").fast(2) +.sound("user") +.partials(randL(10)) +._scope()`} +/> + +and with lists _of_ patterns: + +>".fast(4)) +.sound("user") +.partials([1, 0, "0 1", "0 1 0.3", rand]) +._scope()`} +/> + +Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0. + +#### Phases + +Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built. + +These phases too can be declared in Strudel and can give your sounds interesting depth. + + ## Vibrato