diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index bcbac6254..6483880b1 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2046,6 +2046,28 @@ export const { octave, oct } = registerControl('octave', 'oct'); * ) */ export const { orbit } = registerControl('orbit', 'o'); + +/** + * A `bus` is a send which can be used for mixing patterns. It combines with.. + * s("bus") to play that bus through another pattern (for, say, applying non-linear + * effects like distortion to multiple signals) + * + * otherPat.bmod(..) (to modulate another pattern with the bus) + * + * @name bus + * @param {number | Pattern} number + */ +export const { bus } = registerControl('bus'); + +/** + * Postgain multiplier prior to sending the signal to the audio bus. + * + * @name busgain + * @synonyms bgain + * @param {number | Pattern} number + */ +export const { busgain, bgain } = registerControl('busgain', 'bgain'); + // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that export const { overgain } = registerControl('overgain'); // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1 diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 986ac9af4..b0420a2e6 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3743,18 +3743,18 @@ addConfigAlias('env', 'depthabs', 'da'); addConfigAlias('env', 'acurve', 'ac'); addConfigAlias('env', 'dcurve', 'dc'); addConfigAlias('env', 'rcurve', 'rc'); -addConfigAlias('omod', 'orbit', 'o'); -addConfigAlias('omod', 'target', 't'); -addConfigAlias('omod', 'depth', 'dep', 'dr'); -addConfigAlias('omod', 'depthabs', 'da'); -addConfigAlias('omod', 'dc'); +addConfigAlias('bmod', 'orbit', 'o'); +addConfigAlias('bmod', 'target', 't'); +addConfigAlias('bmod', 'depth', 'dep', 'dr'); +addConfigAlias('bmod', 'depthabs', 'da'); +addConfigAlias('bmod', 'dc'); Pattern.prototype.modulate = function (type, config, idx) { if (config == null || typeof config !== 'object') { return this; } - if (!['lfo', 'env', 'omod'].includes(type)) { - logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'omod'`); + if (!['lfo', 'env', 'bmod'].includes(type)) { + logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); return this; } let output = this; @@ -3828,13 +3828,15 @@ Pattern.prototype.env = function (config, idx) { export const env = (config) => pure({}).env(config); /** - * Modulates with the output from a given `orbit` - * Can be called in sequence like pat.obus(...).obus(...) to set up multiple modulators + * Modulates with the output from a given `bus`. + * Can be called in sequence like pat.bmod(...).bmod(...) to set up multiple modulators * - * @name omod + * Send to an audio bus with `otherPat.bus(..)`. + * + * @name bmod * @memberof Pattern - * @param {Object} config Orbit bus configuration. - * @param {string | Pattern} [config.orbit] Orbit to get modulation signal from + * @param {Object} config Bus modulation configuration. + * @param {string | Pattern} [config.bus] Bus to get modulation signal from * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da @@ -3842,7 +3844,7 @@ export const env = (config) => pure({}).env(config); * @param {number | Pattern} [config.dc] DC offset prior to application * @returns Pattern */ -Pattern.prototype.omod = function (config, idx) { - return this.modulate('omod', config, idx); +Pattern.prototype.bmod = function (config, idx) { + return this.modulate('bmod', config, idx); }; -export const omod = (config) => pure({}).omod(config); +export const bmod = (config) => pure({}).bmod(config); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f32a17168..32d7f0d63 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -163,6 +163,7 @@ let defaultDefaultValues = { distortvol: 1, distorttype: 0, delay: 0, + busgain: 1, byteBeatExpression: '0', delayfeedback: 0.5, delaysync: 3 / 16, @@ -521,10 +522,10 @@ function connectEnvelope(idx, params, nodeTracker, value) { return envNode; } -function connectOrbitModulator(params, nodeTracker, value) { +function connectBusModulator(params, nodeTracker, value) { const ac = getAudioContext(); const { target, depth = 1, depthabs } = params; - const signal = controller.getOrbit(params.orbit).output; + const signal = controller.getBus(params.bus).output; const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); const shifted = dc.connect(gainNode(1)); @@ -628,6 +629,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) delaysync = getDefaultValue('delaysync'), delaytime, orbit = getDefaultValue('orbit'), + bus, + busgain = getDefaultValue('busgain'), room, roomfade, roomlp, @@ -666,6 +669,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) delay = applyGainCurve(delay); velocity = applyGainCurve(velocity); tremolodepth = applyGainCurve(tremolodepth); + busgain = applyGainCurve(busgain); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future const end = t + hapDuration; @@ -970,6 +974,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const reverbSend = orbitBus.sendReverb(post, room); audioNodes.push(reverbSend); } + if (bus != null) { + const busNode = audioController.getBus(bus); + const busSend = effectSend(post, busNode, busgain); + audioNodes.push(busSend); + } if (djf != null) { nodes['djf'] = orbitBus.getDjf(djf, t); @@ -1027,9 +1036,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) audioNodes.push(env); } } - if (value.omod) { - for (const p of value.omod) { - const { toCleanup } = connectOrbitModulator({ ...p, begin: t, end: endWithRelease }, nodes, value); + if (value.bmod) { + for (const p of value.bmod) { + const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes, value); audioNodes.push(...toCleanup); } } diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index 1f3235e59..8284876e9 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -72,6 +72,8 @@ const CONTROL_DATA = { // roomfade: { param: 'room.fade', min: 0, max: 1 }, roomlp: { param: 'room.lp', min: 20, max: 24000 }, djf: { param: 'djf.value', min: 0, max: 1 }, + busgain: { param: 'bus.gain', default: 1, min: 0, max: 10 }, + bgain: { param: 'bus.gain', default: 1, min: 0, max: 10 }, // SYNTHS detune: { param: 'source.detune', min: 0, max: 1 }, diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index d8ead7d06..204eb62a4 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -2,7 +2,10 @@ import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; import { clamp } from './util.mjs'; -let hasChanged = (now, before) => now !== undefined && now !== before; +const hasChanged = (now, before) => now !== undefined && now !== before; +// Node with fixed stereo channel count to prevent clicking when the input signal +// switches from mono to stereo +const getStereoNode = (ac) => new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); export class Orbit { reverbNode; @@ -11,10 +14,11 @@ export class Orbit { summingNode; djfNode; audioContext; + constructor(audioContext) { this.audioContext = audioContext; - this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); - this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.output = getStereoNode(audioContext); + this.summingNode = getStereoNode(audioContext); this.summingNode.connect(this.output); } @@ -164,6 +168,7 @@ export class SuperdoughAudioController { audioContext; output; nodes = {}; + buses = {}; constructor(audioContext) { this.audioContext = audioContext; @@ -171,10 +176,14 @@ export class SuperdoughAudioController { } reset() { - Array.from(this.nodes).forEach((node) => { + Object.values(this.nodes).forEach((node) => { node.disconnect(); }); + Object.values(this.buses).forEach((bus) => { + bus.disconnect(); + }); this.nodes = {}; + this.buses = {}; this.output.reset(); } @@ -206,4 +215,11 @@ export class SuperdoughAudioController { } return this.nodes[orbitNum]; } + + getBus(busNum) { + if (this.buses[busNum] == null) { + this.buses[busNum] = getStereoNode(this.audioContext); + } + return this.buses[busNum]; + } } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index cffbdc0ca..3686e13d1 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,5 @@ import { clamp } from './util.mjs'; -import { registerSound, soundMap } from './superdough.mjs'; +import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, @@ -367,6 +367,41 @@ export function registerSynthSounds() { { prebake: true, type: 'synth' }, ); + registerSound( + 'bus', + (begin, value, onended) => { + const ac = getAudioContext(); + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + ); + const holdend = begin + value.duration; + const end = holdend + release + 0.01; + const bus = getSuperdoughAudioController().getBus(value.n ?? 0); + const envGain = bus.connect(gainNode(1)); + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + const timeoutNode = webAudioTimeout( + ac, + () => { + bus.disconnect(envGain); + onended(); + }, + begin, + end, + ); + + return { + node: envGain, + source: bus, + stop: (time) => { + timeoutNode.stop(time); + }, + }; + }, + { prebake: true, type: 'input' }, + ); + [...noises].forEach((s) => { registerSound( s,