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

Wave Selection and Global Envelope on the FM Synth Modulator #683

Merged
merged 21 commits into from
Aug 31, 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
63 changes: 61 additions & 2 deletions packages/core/controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ const generic_params = [
* @name fmh
* @param {number | Pattern} harmonicity
* @example
* note("c e g b").fm(4).fmh("<1 2 1.5 1.61>")
* note("c e g b")
* .fm(4)
* .fmh("<1 2 1.5 1.61>")
*
*/
[['fmh', 'fmi'], 'fmh'],
Expand All @@ -130,10 +132,67 @@ const generic_params = [
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
* note("c e g b").fm("<0 1 2 8 32>")
* note("c e g b")
* .fm("<0 1 2 8 32>")
*
*/
[['fmi', 'fmh'], 'fm'],
// fm envelope
/**
* Ramp type of fm envelope. Exp might be a bit broken..
*
* @name fmenv
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.2)
* .fmsustain(0)
* .fmenv("<exp lin>")
*
*/
['fmenv'],
/**
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
* @param {number | Pattern} time attack time
* @example
* note("c e g b")
* .fm(4)
* .fmattack("<0 .05 .1 .2>")
*
*/
['fmattack'],
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
* @name fmdecay
* @param {number | Pattern} time decay time
* @example
* note("c e g b")
* .fm(4)
* .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4)
*
*/
['fmdecay'],
/**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
* @param {number | Pattern} level sustain level
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>")
*
*/
['fmsustain'],
// these are not really useful... skipping for now
['fmrelease'],
['fmvelocity'],

/**
* 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
16 changes: 16 additions & 0 deletions packages/superdough/helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ export const getEnvelope = (attack, decay, sustain, release, velocity, begin) =>
};
};

export const getExpEnvelope = (attack, decay, sustain, release, velocity, begin) => {
sustain = Math.max(0.001, sustain);
velocity = Math.max(0.001, velocity);
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0.0001, begin);
gainNode.gain.exponentialRampToValueAtTime(velocity, begin + attack);
gainNode.gain.exponentialRampToValueAtTime(sustain * velocity, begin + attack + decay);
return {
node: gainNode,
stop: (t) => {
// similar to getEnvelope, this will glitch if sustain level has not been reached
gainNode.gain.exponentialRampToValueAtTime(0.0001, t + release);
},
};
};

export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
Expand Down
42 changes: 36 additions & 6 deletions packages/superdough/synth.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getEnvelope } from './helpers.mjs';
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';

const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
Expand All @@ -26,13 +26,20 @@ export function registerSynthSounds() {
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
const {
let {
attack = 0.001,
decay = 0.05,
sustain = 0.6,
release = 0.01,
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
} = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
Expand All @@ -48,15 +55,37 @@ export function registerSynthSounds() {
// make oscillator
const { node: o, stop } = getOscillator({ t, s: wave, freq, partials: n });

let stopFm;
// FM + FM envelope
let stopFm, fmEnvelope;
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex);
modulator.connect(o.frequency);
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
}
stopFm = stop;
}

// turn down
const g = gainNode(0.3);
// envelope

// gain envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);

o.onended = () => {
o.disconnect();
g.disconnect();
Expand All @@ -66,6 +95,7 @@ export function registerSynthSounds() {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
fmEnvelope?.stop(releaseTime);
let end = releaseTime + release;
stop(end);
stopFm?.(end);
Expand Down
84 changes: 84 additions & 0 deletions test/__snapshots__/examples.test.mjs.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1825,6 +1825,69 @@ exports[`runs examples > example "fm" example index 0 1`] = `
]
`;

exports[`runs examples > example "fmattack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmattack:0 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmattack:0 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmattack:0 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmattack:0 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmattack:0.05 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmattack:0.05 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmattack:0.05 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmattack:0.05 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmattack:0.1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmattack:0.1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmattack:0.1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmattack:0.1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmattack:0.2 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmattack:0.2 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmattack:0.2 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmattack:0.2 ]",
]
`;

exports[`runs examples > example "fmdecay" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
]
`;

exports[`runs examples > example "fmenv" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
]
`;

exports[`runs examples > example "fmh" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmh:1 ]",
Expand All @@ -1846,6 +1909,27 @@ exports[`runs examples > example "fmh" example index 0 1`] = `
]
`;

exports[`runs examples > example "fmsustain" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0 ]",
]
`;

exports[`runs examples > example "focus" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:sd ]",
Expand Down
16 changes: 16 additions & 0 deletions website/src/pages/learn/synths.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,20 @@ Now we not only pattern the notes, but the sound as well!

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

### fmattack

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

### fmdecay

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

### fmsustain

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

### fmenv

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

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