Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic fm #669

Merged
merged 5 commits into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/core/controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*
Expand Down
40 changes: 37 additions & 3 deletions packages/superdough/synth.mjs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand All @@ -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);
},
};
},
Expand Down
42 changes: 42 additions & 0 deletions test/__snapshots__/examples.test.mjs.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]",
Expand Down
10 changes: 10 additions & 0 deletions website/src/pages/learn/synths.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

<JsDoc client:idle name="fm" h={0} />

### fmh

<JsDoc client:idle name="fmh" h={0} />

Next up: [Audio Effects](/learn/effects)...