Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit 43504d1

Browse files
committed
add chorus
1 parent 7e8206d commit 43504d1

File tree

2 files changed

+71
-4
lines changed

2 files changed

+71
-4
lines changed

packages/core/controls.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
285285
*/
286286
export const { bank } = registerControl('bank');
287287

288+
/**
289+
* mix control for the chorus effect
290+
*
291+
* @name chorus
292+
* @param {string | Pattern} chorus mix amount between 0 and 1
293+
* @example
294+
* note("d d a# a").s("sawtooth").chorus(.5)
295+
*
296+
*/
297+
export const { chorus } = registerControl('chorus');
298+
288299
// analyser node send amount 0 - 1 (used by scope)
289300
export const { analyze } = registerControl('analyze');
290301
// fftSize of analyser

packages/supradough/dough.mjs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ let gainCurveFunc = (val) => Math.pow(val, 2);
77
function applyGainCurve(val) {
88
return gainCurveFunc(val);
99
}
10+
11+
/**
12+
* Equal Power Crossfade function.
13+
* Smoothly transitions between signals A and B, maintaining consistent perceived loudness.
14+
*
15+
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
16+
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
17+
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
18+
* @returns {number} Crossfaded output value.
19+
*/
20+
function crossfade(a, b, m) {
21+
const aGain = Math.sin((1 - m) * 0.5 * Math.PI);
22+
const bGain = Math.sin(m * 0.5 * Math.PI);
23+
return a * aGain + b * bGain;
24+
}
25+
1026
// function setGainCurve(newGainCurveFunc) {
1127
// gainCurveFunc = newGainCurveFunc;
1228
// }
@@ -349,7 +365,8 @@ export class ADSR {
349365
.add(x=>x.delay(.1).mul(.8))
350366
.out()*/
351367
const MAX_DELAY_TIME = 10;
352-
export class Delay {
368+
export class PitchDelay {
369+
lpf = new TwoPoleFilter();
353370
constructor(_props = {}) {
354371
this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE);
355372
this.writeIdx = 0;
@@ -374,7 +391,38 @@ export class Delay {
374391
} else {
375392
index = Math.floor(this.readIdx * speed) % this.numSamples;
376393
}
377-
return this.buffer[index];
394+
const s = this.lpf.update(this.buffer[index], 0.9, 0);
395+
396+
return s;
397+
}
398+
}
399+
400+
export class Delay {
401+
writeIdx = 0;
402+
readIdx = 0;
403+
buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0)
404+
write(s, delayTime) {
405+
this.writeIdx = (this.writeIdx + 1) % this.buffer.length;
406+
this.buffer[this.writeIdx] = s;
407+
// Calculate how far in the past to read
408+
let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
409+
this.readIdx = this.writeIdx - numSamples;
410+
// If past the start of the buffer, wrap around
411+
if (this.readIdx < 0) this.readIdx += this.buffer.length;
412+
}
413+
update(input, delayTime) {
414+
this.write(input, delayTime);
415+
return this.buffer[this.readIdx];
416+
}
417+
}
418+
//TODO: Figure out why clicking at the start off the buffer
419+
export class Chorus {
420+
delay = new Delay();
421+
modulator = new TriOsc();
422+
update(input, mix, delayTime, modulationFreq, modulationDepth) {
423+
const m = this.modulator.update(modulationFreq) * modulationDepth;
424+
const c = this.delay.update(input, delayTime * (1 + m));
425+
return crossfade(input, c, mix);
378426
}
379427
}
380428

@@ -540,6 +588,7 @@ let shapes = {
540588
};
541589

542590
const defaultDefaultValues = {
591+
chorus: 0,
543592
note: 48,
544593
s: 'triangle',
545594
gain: 1,
@@ -616,6 +665,7 @@ export class DoughVoice {
616665
$.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol'));
617666
$.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol'));
618667
$.i = $.i ?? getDefaultValue('i');
668+
$.chorus = $.chorus ?? getDefaultValue('chorus');
619669
$.fft = $.fft ?? getDefaultValue('fft');
620670
$.pan = $.pan ?? getDefaultValue('pan');
621671
$.orbit = $.orbit ?? getDefaultValue('orbit');
@@ -696,6 +746,7 @@ export class DoughVoice {
696746
}
697747

698748
// channelwise effects setup
749+
$._chorus = $.chorus ? [] : null;
699750
$._lpf = $.cutoff ? [] : null;
700751
$._hpf = $.hcutoff ? [] : null;
701752
$._bpf = $.bandf ? [] : null;
@@ -706,6 +757,7 @@ export class DoughVoice {
706757
$._lpf?.push(new TwoPoleFilter());
707758
$._hpf?.push(new TwoPoleFilter());
708759
$._bpf?.push(new TwoPoleFilter());
760+
$._chorus?.push(new Chorus());
709761
$._coarse?.push(new Coarse());
710762
$._crush?.push(new Crush());
711763
$._distort?.push(new Distort());
@@ -786,6 +838,10 @@ export class DoughVoice {
786838
this.out[i] = this._buffers[i].update(freq);
787839
}
788840
this.out[i] = this.out[i] * this.gain * this.velocity;
841+
if (this._chorus) {
842+
const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11);
843+
this.out[i] = c + this.out[i];
844+
}
789845

790846
if (this._lpf) {
791847
this._lpf[i].update(this.out[i], lpf, this.resonance);
@@ -842,8 +898,8 @@ export class Dough {
842898
this.sampleRate = sampleRate;
843899
this.t = Math.floor(currentTime * sampleRate); // samples
844900
// console.log('init dough', this.sampleRate, this.t);
845-
this._delayL = new Delay();
846-
this._delayR = new Delay();
901+
this._delayL = new PitchDelay();
902+
this._delayR = new PitchDelay();
847903
}
848904
loadSample(name, channels, sampleRate) {
849905
BufferPlayer.samples.set(name, { channels, sampleRate });

0 commit comments

Comments
 (0)