Merge pull request #683 from Bubobubobubobubo/betterfmsynth

Wave Selection and Global Envelope on the FM Synth Modulator
This commit is contained in:
Felix Roos 2023-08-31 12:36:36 +02:00 committed by GitHub
commit c54916cdfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 213 additions and 8 deletions

View File

@ -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'],
@ -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`.

View File

@ -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);

View File

@ -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();
@ -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
@ -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();
@ -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);

View File

@ -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 ]",
@ -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 ]",

View File

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