From 0c124bcca378fb2d64ec80053efb95b8a96c270d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 20 Aug 2023 22:32:27 +0200 Subject: [PATCH] basic fm --- packages/core/controls.mjs | 26 +++++++++++++++++++++++ packages/superdough/synth.mjs | 39 +++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) 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..c0b3ef93 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -2,13 +2,39 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound } 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; + const { node: modulator, stop } = mod(modfreq, modgain, wave); + return { node: modulator, stop }; +}; + 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 +48,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 +67,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); }, }; },