diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5cc53de60..1bf3fa85c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2869,8 +2869,8 @@ registerSubControls('bmod', [ ['dc'], ]); -Pattern.prototype.modulate = function (type, config, idx) { - config ??= { control: undefined }; +Pattern.prototype.modulate = function (type, config, id) { + config = { control: undefined, ...config }; const modulatorKeys = ['lfo', 'env', 'bmod']; if (!modulatorKeys.includes(type)) { logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); @@ -2888,19 +2888,20 @@ Pattern.prototype.modulate = function (type, config, idx) { // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO let control = getControlName(Object.keys(v).at(-1)); if (modulatorKeys.includes(control)) { - control = `${control}${v[control].length - 1}`; + control = `${control}_${[...v[control].__ids].at(-1)}`; } defaultValue = control; } - v[type] ??= []; + v[type] ??= { __ids: new Set() }; const t = v[type]; - idx ??= t.length; - t[idx] ??= { control: defaultValue }; + id ??= t.__ids.size; + t[id] ??= { control: defaultValue }; + t.__ids.add(id); // keeps track of insertion order if (c === undefined) return v; if (key === 'control' || key === 'subControl') { - t[idx][key] = getControlName(c); + t[id][key] = getControlName(c); } else { - t[idx][key] = c; + t[id][key] = c; } return v; }) @@ -2910,10 +2911,16 @@ Pattern.prototype.modulate = function (type, config, idx) { }; /** - * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs + * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs. + * There are two ways to declare which control will be modulated: + * 1. Explicitly put `control` in the config (e.g. `lfo({ c: "lpf" })`) + * 2. If the control parameter is absent, the control _immediately before_ the `lfo` call will be used + * (e.g. `s("saw").lpf(500).lfo()` to modulate `lpf`) + * + * Modulators can be referred to by `id` so that they can be updated later e.g. inside + * a `sometimes`. See example below. * * @name lfo - * @memberof Pattern * @param {Object} config LFO configuration. * @param {string | Pattern} [config.control] Node to modulate. Aliases: t * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p @@ -2925,18 +2932,47 @@ Pattern.prototype.modulate = function (type, config, idx) { * @param {number | Pattern} [config.skew] Skew amount. Aliases: sk * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s + * @param {string | Pattern} id ID to use for this modulator * @returns Pattern + * + * @example + * s("saw").note("F1").lpf(500).lfo() + * + * @example + * s("saw").lfo().lpf(500).lfo({ s: 0.3 }) + * + * @example + * s("saw").lpf(500).diode(0.3) + * .lfo({ c: "lpf" }) + * + * @example + * s("pulse").lpf(500).lfo() + * .lfo({ c: "s" }) + * .diode(0.3) + * .sometimes(x => x.lfo({ s: "8" }, 1)) // lfo #1 (0-indexed) + * + * @example + * s("pulse").lpf(500).lfo({ depth: 4 }, 'lpf_mod') + * .lfo({ c: "s" }) + * .diode(0.3) + * .sometimes(x => x.lfo({ s: "8" }, 'lpf_mod')) */ -Pattern.prototype.lfo = function (config, idx) { - return this.modulate('lfo', config, idx); +Pattern.prototype.lfo = function (config, id) { + return this.modulate('lfo', config, id); }; export const lfo = (config) => pure({}).lfo(config); /** * Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes + * There are two ways to declare which control will be modulated: + * 1. Explicitly put `control` in the config (e.g. `env({ c: "lpf" })`) + * 2. If the control parameter is absent, the control _immediately before_ the `env` call will be used + * (e.g. `s("saw").lpf(500).env({ a: 1 })` to modulate `lpf`) + * + * Modulators can be referred to by `id` so that they can be updated later e.g. inside + * a `sometimes`. See example below. * * @name env - * @memberof Pattern * @param {Object} config Envelope configuration. * @param {string | Pattern} [config.control] Node to modulate. Aliases: t * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p @@ -2949,10 +2985,35 @@ export const lfo = (config) => pure({}).lfo(config); * @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac * @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc * @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc + * @param {string | Pattern} id ID to use for this modulator * @returns Pattern + * + * @example + * s("saw").note("F1").lpf(500).env({ a: 1 }) + * + * @example + * s("saw").env({ d: 1 }).note("F1") + * .lpq(4).lpf(50) + * .env({ a: 0.1, d: 1, ac: 0.8, dc: 0.3, depth: 50 }) + * + * @example + * s("saw").lpf(500).diode(0.3) + * .env({ c: "lpf", a: 0.5, d: 0.5 }) + * + * @example + * s("pulse").lpf(500).env({ a: 1 }) + * .env({ c: "s", a: 1 }) + * .diode(0.3) + * .sometimes(x => x.env({ a: "0.5" }, 1)) // envelope #1 (0-indexed) + * + * @example + * s("pulse").lpf(500).env({ a: 1 }, 'lpf_mod') + * .env({ c: "s", a: 1 }) + * .diode(0.3) + * .sometimes(x => x.env({ a: "0.5" }, 'lpf_mod')) */ -Pattern.prototype.env = function (config, idx) { - return this.modulate('env', config, idx); +Pattern.prototype.env = function (config, id) { + return this.modulate('env', config, id); }; export const env = (config) => pure({}).env(config); @@ -2962,8 +3023,15 @@ export const env = (config) => pure({}).env(config); * * Send to an audio bus with `otherPat.bus(..)`. * + * There are two ways to declare which control will be modulated: + * 1. Explicitly put `control` in the config (e.g. `bmod({ id: 2, c: "lpf" })`) + * 2. If the control parameter is absent, the control _immediately before_ the `bmod` call will be used + * (e.g. `s("saw").lpf(500).bmod({ id: 2 })` to modulate `lpf`) + * + * Modulators can be referred to by `id` so that they can be updated later e.g. inside + * a `sometimes`. See example below. + * * @name bmod - * @memberof Pattern * @param {Object} config Bus modulation configuration. * @param {string | Pattern} [config.bus] Bus to get modulation signal from * @param {string | Pattern} [config.control] Node to modulate. Aliases: t @@ -2971,10 +3039,16 @@ export const env = (config) => pure({}).env(config); * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.dc] DC offset prior to application + * @param {string | Pattern} id ID to use for this modulator * @returns Pattern + * + * @example + * modulator: s("one").seg(64).gain(slider(0, 0, 1)).bus(1).dry(0) + * carrier: s("saw").bmod({ b: 1 }) + * */ -Pattern.prototype.bmod = function (config, idx) { - return this.modulate('bmod', config, idx); +Pattern.prototype.bmod = function (config, id) { + return this.modulate('bmod', config, id); }; export const bmod = (config) => pure({}).bmod(config); diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index 3411aacb7..32ebae265 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -8,6 +8,7 @@ import { getAudioContext } from './audioContext.mjs'; import { gainNode, getEnvelope, getLfo, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; import { getSuperdoughControlTargets } from './superdoughdata.mjs'; +import { clamp } from './util.mjs'; const getNodeParam = (node, name) => { // Worklet case @@ -27,9 +28,8 @@ const getNodeParam = (node, name) => { const controlTargets = getSuperdoughControlTargets(); -const stripIndex = (control) => control?.replace(/\d+$/, ''); const getControlData = (control) => { - return controlTargets[stripIndex(control)]; + return controlTargets[control.split('_')[0]]; }; const getRangeForParam = (paramName, currentValue) => { @@ -41,6 +41,19 @@ const getRangeForParam = (paramName, currentValue) => { return { min: undefined, max: undefined }; }; +const clampWithWaveShaper = (modulator, min, max) => { + const ac = getAudioContext(); + const curve = new Float32Array(256); + for (let i = 0; i < curve.length; i++) { + const x = (i / (curve.length - 1)) * 2 - 1; + curve[i] = clamp(x * max, min, max); + } + const shaper = new WaveShaperNode(ac, { curve }); + const scaleGain = gainNode(1 / max); + modulator.connect(scaleGain).connect(shaper); + return { modulator, toCleanup: [shaper, scaleGain] }; +}; + const getTargetParamsForControl = (control, nodes, subControl) => { const lookupKey = subControl ? `${control}_${subControl}` : control; const targetInfo = getControlData(lookupKey) ?? getControlData(control); @@ -67,7 +80,7 @@ const getTargetParamsForControl = (control, nodes, subControl) => { return { targetParams: audioParams, paramName }; }; -export const connectLFO = (idx, params, nodeTracker) => { +export const connectLFO = (id, params, nodeTracker) => { const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); if (!targetParams.length) return; @@ -84,12 +97,12 @@ export const connectLFO = (idx, params, nodeTracker) => { max, }; const lfoNode = getLfo(getAudioContext(), modParams); - nodeTracker[`lfo${idx}`] = [lfoNode]; + nodeTracker[`lfo_${id}`] = [lfoNode]; targetParams.forEach((t) => lfoNode.connect(t)); return lfoNode; }; -export const connectEnvelope = (idx, params, nodeTracker) => { +export const connectEnvelope = (id, params, nodeTracker) => { const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); if (!targetParams.length) return; @@ -106,7 +119,7 @@ export const connectEnvelope = (idx, params, nodeTracker) => { decayCurve: dcurve, releaseCurve: rcurve, }); - nodeTracker[`env${idx}`] = [envNode]; + nodeTracker[`env_${id}`] = [envNode]; targetParams.forEach((t) => envNode.connect(t)); return envNode; }; @@ -122,13 +135,12 @@ export const connectBusModulator = (params, nodeTracker, controller) => { const shifted = dc.connect(gainNode(1)); signal.connect(shifted); let currentValue = targetParams[0].value; - debugger; currentValue = currentValue === 0 ? 1 : currentValue; const { min, max } = getRangeForParam(paramName, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; - const maxAbsDepth = Math.min(Math.abs(min), Math.abs(max)); - const boundedDepth = Math.min(Math.abs(depthValue), maxAbsDepth) || Math.abs(depthValue); - const modulator = shifted.connect(gainNode((Math.sign(depthValue) * boundedDepth) / 0.3)); + const depthGain = gainNode((Math.sign(depthValue) * Math.abs(depthValue)) / 0.3); + const unClamped = shifted.connect(depthGain); + let { modulator, toCleanup } = clampWithWaveShaper(unClamped, min, max); webAudioTimeout( ac, () => { @@ -137,5 +149,6 @@ export const connectBusModulator = (params, nodeTracker, controller) => { 0, params.begin, ); - return { modulator, toCleanup: [dc, shifted, modulator] }; + toCleanup.push(dc, shifted, depthGain); + return { modulator, toCleanup }; }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 517b1d3e5..ee0cec836 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -875,9 +875,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // finally, now that `nodes` is populated, set up modulators if (value.lfo) { - for (const [idx, params] of Object.entries(value.lfo)) { + for (const id of value.lfo.__ids) { + const params = value.lfo[id]; const lfo = connectLFO( - idx, + id, { ...params, cps, @@ -891,9 +892,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } } if (value.env) { - for (const [idx, params] of Object.entries(value.env)) { + for (const id of value.env.__ids) { + const params = value.env[id]; const env = connectEnvelope( - idx, + id, { ...params, begin: t, @@ -905,8 +907,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } } if (value.bmod) { - for (const p of value.bmod) { - const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes, controller); + for (const id of value.bmod.__ids) { + const params = value.bmod[id]; + const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller); audioNodes.push(...toCleanup); } } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index d20692729..6c8d1fea9 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -19,7 +19,7 @@ import { import { logger } from './logger.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user']; +const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one']; const waveformAliases = [ ['tri', 'triangle'], ['sqr', 'square'], @@ -508,8 +508,17 @@ export function getOscillator(s, t, value, onended) { s = 'triangle'; } s = s === 'user' && !partials ? 'triangle' : s; - // If no partials are given, use stock waveforms - if (!partials || partials?.length === 0 || s === 'sine') { + if (s === 'one') { + // Constant 1 oscillator (used for modulation) + o = new ConstantSourceNode(getAudioContext(), { offset: 1 }); + o.start(t); + return { + node: o, + nodes: { source: o }, + stop: (time) => o?.stop(time), + }; + } else if (!partials || partials?.length === 0 || s === 'sine') { + // If no partials are given, use stock waveforms o = getAudioContext().createOscillator(); o.type = s || 'triangle'; } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 5f8133ec5..61ab11866 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3803,6 +3803,51 @@ exports[`runs examples > example "end" example index 0 1`] = ` ] `; +exports[`runs examples > example "env" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", +] +`; + +exports[`runs examples > example "env" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", + "[ 1/1 → 2/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", + "[ 2/1 → 3/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", + "[ 3/1 → 4/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", +] +`; + +exports[`runs examples > example "env" example index 2 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", +] +`; + +exports[`runs examples > example "env" example index 3 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + +exports[`runs examples > example "env" example index 4 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + exports[`runs examples > example "euclid" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 ]", @@ -6113,6 +6158,51 @@ exports[`runs examples > example "leslie" example index 0 1`] = ` ] `; +exports[`runs examples > example "lfo" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", +] +`; + +exports[`runs examples > example "lfo" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", + "[ 1/1 → 2/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", + "[ 2/1 → 3/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", + "[ 3/1 → 4/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", +] +`; + +exports[`runs examples > example "lfo" example index 2 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", +] +`; + +exports[`runs examples > example "lfo" example index 3 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + +exports[`runs examples > example "lfo" example index 4 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + exports[`runs examples > example "linger" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:lt ]", diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 896a02f8a..ebec2252e 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,6 +20,7 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', + 'bmod', ]; describe('runs examples', () => {