diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 717a8353..c6dbc614 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -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("") + * + */ + ['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`. diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 651fc85c..07e2b121 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -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); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 9ad347e3..a409d990 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -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); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ee937e2b..787bc461 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -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 ]", diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 6ebf2613..9c515752 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -38,4 +38,20 @@ Now we not only pattern the notes, but the sound as well! +### fmattack + + + +### fmdecay + + + +### fmsustain + + + +### fmenv + + + Next up: [Audio Effects](/learn/effects)...