From b3f8df17838cabb2a0c78bff529a2bdf03421341 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sat, 4 Nov 2023 20:20:25 -0400 Subject: [PATCH] initial commit --- packages/core/controls.mjs | 11 ++++ packages/superdough/phaser.mjs | 88 ++++++++++++++++++++++++++++++ packages/superdough/superdough.mjs | 10 ++++ packages/superdough/worklets.mjs | 33 +++++++++++ 4 files changed, 142 insertions(+) create mode 100644 packages/superdough/phaser.mjs diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 8841b4bc..a616ce06 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -380,6 +380,17 @@ const generic_params = [ * */ ['coarse'], + + /** + * fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers + * + * @name phaser + * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. + * @example + * s("bd sd,hh*4").coarse("<1 4 8 16 32>") + * + */ + ['phaser'], /** * choose the channel the pattern is sent to in superdirt * diff --git a/packages/superdough/phaser.mjs b/packages/superdough/phaser.mjs new file mode 100644 index 00000000..62771df3 --- /dev/null +++ b/packages/superdough/phaser.mjs @@ -0,0 +1,88 @@ +// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597 +export var vowelFormant = { + 0: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] }, + 1: { freqs: [440, 1800, 2700, 3000, 3300], gains: [1, 0.1995, 0.1259, 0.1, 0.1], qs: [70, 80, 100, 120, 120] }, + 2: { freqs: [270, 1850, 2900, 3350, 3590], gains: [1, 0.0631, 0.0631, 0.0158, 0.0158], qs: [40, 90, 100, 120, 120] }, + 3: { freqs: [430, 820, 2700, 3000, 3300], gains: [1, 0.3162, 0.0501, 0.0794, 0.01995], qs: [40, 80, 100, 120, 120] }, + 4: { freqs: [370, 630, 2750, 3000, 3400], gains: [1, 0.1, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] }, +}; + +var createFilter = function (ctx, cutoff, Q) { + var lowpassFilter = ctx.createBiquadFilter(); + lowpassFilter.type = 'notch'; + lowpassFilter.gain.value = 1; + lowpassFilter.frequency.value = cutoff; + lowpassFilter.Q.value = Q; + return lowpassFilter; +}; + +// var createTriOscillator = function (freq) { +// var osc = ctx.createOscillator(); +// osc.type = 'triangle'; +// osc.frequency.value = freq * 1.0; +// osc.detune.value = 0; +// return osc; +// }; + +var createLFO = function (ctx, freq) { + var osc = ctx.createOscillator(); + osc.frequency.value = freq; + osc.type = 'sine'; + osc.start(); + return osc; +}; +var createLFOGain = function (ctx, gain) { + var gainNode = ctx.createGain(); + gainNode.gain.value = gain; + return gainNode; +}; +let lfo, lfoGain; +if (typeof GainNode !== 'undefined') { + class PhaserNode extends GainNode { + constructor(ac, speed) { + super(ac); + console.log('speed', speed); + + if (!vowelFormant[speed]) { + throw new Error('phaser: unknown phaser ' + speed); + } + const { gains, qs, freqs } = vowelFormant[speed]; + const makeupGain = ac.createGain(); + + // var sine = ac.createOscillator(), + // sineGain = ac.createGain(); + + // //set up our oscillator types + // sine.type = sine.SINE; + + // //set the amplitude of the modulation + // sineGain.gain.value = 100; + + // //connect the dots + // sine.connect(sineGain); + if (lfo == null) { + lfo = createLFO(ac, 0.25); + lfoGain = createLFOGain(ac, 4000); + + lfo.connect(lfoGain); + } + // sineGain.connect(saw.frequency); + for (let i = 0; i < 6; i++) { + const gain = ac.createGain(); + gain.gain.value = 0.5; + const filter = createFilter(ac, 1000 + i * 20, 1); + this.connect(filter); + lfoGain.connect(filter.detune); + filter.connect(gain); + gain.connect(makeupGain); + } + makeupGain.gain.value = 1; // how much makeup gain to add? + this.connect = (target) => makeupGain.connect(target); + return this; + } + } + + AudioContext.prototype.createPhaser = function (speed) { + return new PhaserNode(this, speed); + }; +} diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e3033afe..3a215604 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,6 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; +import './phaser.mjs'; import { clamp } from './util.mjs'; import workletsUrl from './worklets.mjs?url'; import { createFilter, gainNode, getCompressor } from './helpers.mjs'; @@ -226,6 +227,9 @@ export const superdough = async (value, deadline, hapDuration) => { bpsustain = 1, bprelease = 0.01, bandq = 1, + + //phaser + phaser, // coarse, crush, @@ -361,10 +365,16 @@ export const superdough = async (value, deadline, hapDuration) => { chain.push(vowelFilter); } + if (phaser !== undefined) { + const phaserFX = ac.createPhaser(phaser); + chain.push(phaserFX); + } + // effects coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape })); + // phaser !== undefined && chain.push(getWorklet(ac, 'phaser-processor', { phaser })); compressorThreshold !== undefined && chain.push( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 7bb43f87..cce52453 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -106,3 +106,36 @@ class ShapeProcessor extends AudioWorkletProcessor { } registerProcessor('shape-processor', ShapeProcessor); + +// class PhaseProcessor extends AudioWorkletProcessor { +// static get parameterDescriptors() { +// return [{ name: 'phaser', defaultValue: 0 }]; +// } + +// constructor() { +// super(); +// this.notStarted = true; +// } + +// process(inputs, outputs, parameters) { +// const input = inputs[0]; +// const output = outputs[0]; +// const phaser0 = parameters.phaser[0]; +// const phaser1 = phaser0 < 1 ? phaser0 : 1.0 - 4e-10; +// const phaser = (2.0 * phaser1) / (1.0 - phaser1); +// const blockSize = 128; +// const hasInput = !(input[0] === undefined); +// if (hasInput) { +// this.notStarted = false; +// for (let n = 0; n < blockSize; n++) { +// const value = ((1 + phaser) * input[0][n]) / (1 + phaser * Math.abs(input[0][n])); +// for (let o = 0; o < output.length; o++) { +// output[o][n] = value; +// } +// } +// } +// return this.notStarted || hasInput; +// } +// } + +// registerProcessor('phaser-processor', PhaseProcessor);