diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index d83e271f..b8edd51c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -109,6 +109,32 @@ const generic_params = [ */ ['attack', 'att'], + /** + * Sets the Frequency Modulation Harmonicity Ratio. + * Controls the timbre of the sound. + * Whole numbers and simple ratios sound more natural, + * while decimal numbers and complex ratios sound metallic. + * + * @name fmh + * @param {number | Pattern} harmonicity + * @example + * note("c e g b").fm(4).fmh("<1 2 1.5 1.61>") + * + */ + [['fmh', 'fmi'], 'fmh'], + /** + * Sets the Frequency Modulation of the synth. + * Controls the modulation index, which defines the brightness of the sound. + * + * @name fm + * @param {number | Pattern} brightness modulation index + * @synonyms fmi + * @example + * note("c e g b").fm("<0 1 2 8 32>") + * + */ + [['fmi', 'fmh'], 'fm'], + /** * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. * diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 73174f65..57317133 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,14 +1,39 @@ import { midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound } from './superdough.mjs'; +import { registerSound, getAudioContext } from './superdough.mjs'; import { getOscillator, gainNode, getEnvelope } from './helpers.mjs'; +const mod = (freq, range = 1, type = 'sine') => { + const ctx = getAudioContext(); + const osc = ctx.createOscillator(); + osc.type = type; + osc.frequency.value = freq; + osc.start(); + const g = new GainNode(ctx, { gain: range }); + osc.connect(g); // -range, range + return { node: g, stop: (t) => osc.stop(t) }; +}; + +const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => { + const carrfreq = osc.frequency.value; + const modfreq = carrfreq * harmonicityRatio; + const modgain = modfreq * modulationIndex; + return mod(modfreq, modgain, wave); +}; + export function registerSynthSounds() { ['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => { registerSound( wave, (t, value, onended) => { // destructure adsr here, because the default should be different for synths and samples - const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value; + const { + attack = 0.001, + decay = 0.05, + sustain = 0.6, + release = 0.01, + fmh: fmHarmonicity = 1, + fmi: fmModulationIndex, + } = value; let { n, note, freq } = value; // with synths, n and note are the same thing n = note || n || 36; @@ -22,6 +47,13 @@ export function registerSynthSounds() { // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) // make oscillator const { node: o, stop } = getOscillator({ t, s: wave, freq }); + + let stopFm; + if (fmModulationIndex) { + const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex); + modulator.connect(o.frequency); + stopFm = stop; + } const g = gainNode(0.3); // envelope const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); @@ -34,7 +66,9 @@ export function registerSynthSounds() { node: o.connect(g).connect(envelope), stop: (releaseTime) => { releaseEnvelope(releaseTime); - stop(releaseTime + release); + let end = releaseTime + release; + stop(end); + stopFm?.(end); }, }; }, diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 967d6b5d..ee937e2b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1804,6 +1804,48 @@ exports[`runs examples > example "floor" example index 0 1`] = ` ] `; +exports[`runs examples > example "fm" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c fmi:0 ]", + "[ 1/4 → 1/2 | note:e fmi:0 ]", + "[ 1/2 → 3/4 | note:g fmi:0 ]", + "[ 3/4 → 1/1 | note:b fmi:0 ]", + "[ 1/1 → 5/4 | note:c fmi:1 ]", + "[ 5/4 → 3/2 | note:e fmi:1 ]", + "[ 3/2 → 7/4 | note:g fmi:1 ]", + "[ 7/4 → 2/1 | note:b fmi:1 ]", + "[ 2/1 → 9/4 | note:c fmi:2 ]", + "[ 9/4 → 5/2 | note:e fmi:2 ]", + "[ 5/2 → 11/4 | note:g fmi:2 ]", + "[ 11/4 → 3/1 | note:b fmi:2 ]", + "[ 3/1 → 13/4 | note:c fmi:8 ]", + "[ 13/4 → 7/2 | note:e fmi:8 ]", + "[ 7/2 → 15/4 | note:g fmi:8 ]", + "[ 15/4 → 4/1 | note:b fmi:8 ]", +] +`; + +exports[`runs examples > example "fmh" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c fmi:4 fmh:1 ]", + "[ 1/4 → 1/2 | note:e fmi:4 fmh:1 ]", + "[ 1/2 → 3/4 | note:g fmi:4 fmh:1 ]", + "[ 3/4 → 1/1 | note:b fmi:4 fmh:1 ]", + "[ 1/1 → 5/4 | note:c fmi:4 fmh:2 ]", + "[ 5/4 → 3/2 | note:e fmi:4 fmh:2 ]", + "[ 3/2 → 7/4 | note:g fmi:4 fmh:2 ]", + "[ 7/4 → 2/1 | note:b fmi:4 fmh:2 ]", + "[ 2/1 → 9/4 | note:c fmi:4 fmh:1.5 ]", + "[ 9/4 → 5/2 | note:e fmi:4 fmh:1.5 ]", + "[ 5/2 → 11/4 | note:g fmi:4 fmh:1.5 ]", + "[ 11/4 → 3/1 | note:b fmi:4 fmh:1.5 ]", + "[ 3/1 → 13/4 | note:c fmi:4 fmh:1.61 ]", + "[ 13/4 → 7/2 | note:e fmi:4 fmh:1.61 ]", + "[ 7/2 → 15/4 | note:g fmi:4 fmh:1.61 ]", + "[ 15/4 → 4/1 | note:b fmi:4 fmh:1.61 ]", +] +`; + exports[`runs examples > example "focus" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:sd ]", diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 3f808e66..6ebf2613 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -28,4 +28,14 @@ The power of patterns allows us to sequence any _param_ independently: Now we not only pattern the notes, but the sound as well! `sawtooth` `square` and `triangle` are the basic waveforms available in `s`. +## FM Synthesis + +### fm + + + +### fmh + + + Next up: [Audio Effects](/learn/effects)...