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)...