Skip to content

Commit

Permalink
Merge pull request #5 from klknn/chorus
Browse files Browse the repository at this point in the history
Implement a chorus/flanger module.
  • Loading branch information
klknn authored Aug 26, 2021
2 parents ded4772 + 756e5a0 commit c3d06e8
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 16 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ https://github.com/AuburnSounds/Dplug/wiki/Getting-Started
- [x] Saturation
- [x] GUI
- [x] LFO
- [x] Effect
- [x] Effect (phaser is WIP)
- [x] Equalizer / Pan
- [x] Voice
- [ ] Arpeggiator
- [ ] Tempo Delay
- [x] Tempo Delay
- [ ] Chorus / Flanger
- [ ] Presets
- [ ] MIDI
Expand Down
99 changes: 99 additions & 0 deletions source/synth2/chorus.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
module synth2.chorus;

import dplug.client.client : TimeInfo;

import synth2.delay : Delay, DelayKind;
import synth2.lfo : LFO, Multiplier;
import synth2.waveform : Waveform;


struct Chorus {
@nogc nothrow:

void setSampleRate(float sampleRate) {
_lfo.setSampleRate(sampleRate);
_delay.setSampleRate(sampleRate);
}

void setParams(float msecs, float feedback, float depth, float rate) {
_depth = depth;
_msecs = msecs;
_feedback = feedback;
_lfo.setParams(Waveform.sine, false, rate / 10, Multiplier.none, TimeInfo.init);
}

float[2] apply(float[2] x...) {
auto msecsMod = _msecs + (_lfo.front + 1) * _depth;
_lfo.popFront();
_delay.setParams(DelayKind.st, msecsMod * 1e-3, 0, _feedback);
return _delay.apply(x);
}

private:
float _depth, _msecs, _feedback;
Delay _delay;
LFO _lfo;
}


struct MultiChorus {
@nogc nothrow:

static immutable offsetMSecs = [0.55, 0.64, 12.5, 26.4, 18.4];

void setSampleRate(float sampleRate) {
foreach (ref c; _chorus) {
c.setSampleRate(sampleRate);
}
}

void setParams(int numActive, float width,
float msecs, float feedback, float depth, float rate) {
_numActive = numActive;
_width = width;
foreach (i, ref c; _chorus) {
c.setParams(msecs + offsetMSecs[i], feedback, depth, rate);
}
}

float[2] apply(float[2] x...) {
float[2] y;
y[] = 0;
if (_width == 0 || _numActive == 1) {
foreach (i; 0 .. _numActive) {
y[] += _chorus[i].apply(x)[];
}
}
// Wide stereo panning.
else {
const width = _width / 2 + 0.5; // range [0.5, 1.0]
if (_numActive >= 2) {
const c0 = _chorus[0].apply(x);
y[0] += width * c0[0];
y[1] += (1 - width) * c0[1];
const c1 = _chorus[1].apply(x);
y[0] += (1 - width) * c1[0];
y[1] += width * c1[1];
}
if (_numActive == 3) {
y[] += _chorus[2].apply(x)[];
}
if (_numActive == 4) {
const halfWidth = _width / 2 + 0.5; // range [0.5, 0.75]
const c2 = _chorus[2].apply(x);
y[0] += halfWidth * c2[0];
y[1] += (1 - halfWidth) * c2[1];
const c3 = _chorus[3].apply(x);
y[0] += (1 - halfWidth) * c3[0];
y[1] += halfWidth * c3[1];
}
}
y[] /= _numActive;
return y;
}

private:
float _width;
int _numActive;
Chorus[4] _chorus;
}
49 changes: 49 additions & 0 deletions source/synth2/client.d
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import dplug.client.params : Parameter;
import dplug.client.midi : MidiMessage, makeMidiMessageNoteOn, makeMidiMessageNoteOff;
import mir.math : exp2, log, sqrt, PI, fastmath;

import synth2.chorus : MultiChorus;
import synth2.delay : Delay, DelayKind;
import synth2.equalizer : Equalizer;
import synth2.effect : EffectKind, MultiEffect;
Expand Down Expand Up @@ -98,6 +99,7 @@ class Synth2Client : Client {
_menv.releaseTime = 0;
_effect.setSampleRate(sampleRate);
_delay.setSampleRate(sampleRate);
_chorus.setSampleRate(sampleRate);
_eq.setSampleRate(sampleRate);
foreach (ref lfo; _lfos) {
lfo.setSampleRate(sampleRate);
Expand Down Expand Up @@ -296,6 +298,22 @@ class Synth2Client : Client {
readParam!float(Params.delaySpread),
readParam!float(Params.delayFeedback));
}

// Setup chorus.
// TODO: support Params.chorusMulti and width.
const chorusLevel = convertDecibelToLinearGain(
readParam!float(Params.chorusLevel));
const chorusOn = readParam!bool(Params.chorusOn) && chorusLevel > 0;
if (chorusOn) {
_chorus.setParams(
readParam!int(Params.chorusMulti),
readParam!float(Params.chorusWidth),
readParam!float(Params.chorusTime),
readParam!float(Params.chorusFeedback),
readParam!float(Params.chorusDepth),
readParam!float(Params.chorusRate));
}

// Generate samples.
foreach (frame; 0 .. frames) {
float menvVal = menvAmount * _menv.front;
Expand Down Expand Up @@ -389,6 +407,14 @@ class Synth2Client : Client {
output *= modAmp;
outputs[0][frame] = (1 + modPan) * output;
outputs[1][frame] = (1 - modPan) * output;

if (chorusOn) {
const chorusOuts = _chorus.apply(outputs[0][frame], outputs[1][frame]);
foreach (i; 0 .. outputs.length) {
outputs[i][frame] += chorusLevel * chorusOuts[i];
}
}

if (delayMix != 0) {
const delayOuts = _delay.apply(outputs[0][frame], outputs[1][frame]);
foreach (i; 0 .. outputs.length) {
Expand All @@ -401,6 +427,7 @@ class Synth2Client : Client {
}

private:
MultiChorus _chorus;
Delay _delay;
LFO[nLFO] _lfos;
MultiEffect _effect;
Expand Down Expand Up @@ -814,3 +841,25 @@ unittest {
assert(host.paramChangeOutputs!(Params.voicePortament)(1));
// TODO: assert(host.paramChangeOutputs!(Params.voicePortamentAuto)(false));
}

/// Test Chorus
@nogc nothrow @system
unittest {
TestHost host = { mallocNew!Synth2Client() };
scope (exit) destroyFree(host.client);

host.frames = 1000;
// TODO: test On/Off sound diff.
host.setParam!(Params.chorusOn)(true);
host.setParam!(Params.chorusLevel)(1.0);
host.setParam!(Params.chorusMulti)(2);
assert(host.paramChangeOutputs!(Params.chorusMulti)(1));
assert(host.paramChangeOutputs!(Params.chorusMulti)(3));
assert(host.paramChangeOutputs!(Params.chorusMulti)(4));
assert(host.paramChangeOutputs!(Params.chorusTime)(40));
assert(host.paramChangeOutputs!(Params.chorusDepth)(0.5));
assert(host.paramChangeOutputs!(Params.chorusRate)(20));
assert(host.paramChangeOutputs!(Params.chorusFeedback)(1));
assert(host.paramChangeOutputs!(Params.chorusLevel)(1));
assert(host.paramChangeOutputs!(Params.chorusWidth)(1));
}
98 changes: 84 additions & 14 deletions source/synth2/gui.d
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ class Synth2GUI : PBRBackgroundGUI!(png1, png2, png3, png3, png3, ""), IParamete

enum marginW = 5;
enum marginH = 5;
enum screenWidth = 720;
enum screenHeight = 320;
enum screenWidth = 640;
enum screenHeight = 480;

enum fontLarge = 16;
enum fontMedium = 12;
Expand Down Expand Up @@ -133,6 +133,7 @@ class Synth2GUI : PBRBackgroundGUI!(png1, png2, png3, png3, png3, ""), IParamete
_font = mallocNew!Font(cast(ubyte[])(_fontRaw));

_params[Params.voicePoly].addListener(this);
_params[Params.chorusMulti].addListener(this);

static immutable float[7] ratios = [0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f];
super(makeSizeConstraintsDiscrete(screenWidth, screenHeight, ratios));
Expand All @@ -146,32 +147,37 @@ class Synth2GUI : PBRBackgroundGUI!(png1, png2, png3, png3, png3, ""), IParamete
_tempo = _addLabel("BPM000.0", _date.position.max.x + marginW,
_synth2.position.min.y, fontMedium);

enum marginWSec = marginW * 5;

auto osc = _buildOsc(marginW, _synth2.position.max.y + marginH);

auto master = _buildMaster(osc.max.x + marginW, osc.min.y);
auto master = _buildMaster(osc.max.x + marginWSec, osc.min.y);

auto menv = _buildModEnv(master.min.x, master.max.y + marginH * 3);

auto ampEnv = _buildADSR(master.max.x + marginW, osc.min.y, "AmpEnv",
auto ampEnv = _buildADSR(master.max.x + marginWSec, osc.min.y, "AmpEnv",
Params.ampAttack);

auto filterEnv = _buildADSR(ampEnv.min.x, ampEnv.max.y,
"FilterEnv", Params.filterAttack);

auto filter = _buildFilter(menv.max.x + marginW, menv.min.y);
auto filter = _buildFilter(menv.max.x + marginWSec, menv.min.y);

auto effect = _buildEffect(ampEnv.max.x + marginW, ampEnv.min.y);
auto effect = _buildEffect(ampEnv.max.x + marginWSec, ampEnv.min.y);

auto eq = _buildEQ(effect.max.x + marginW, effect.min.y);
auto eq = _buildEQ(effect.max.x + marginWSec, effect.min.y);

auto delay = _buildDelay(filter.max.x + marginW, effect.max.y + marginH * 3);
auto delay = _buildDelay(filter.max.x + marginWSec, effect.max.y + marginH * 3);

auto voice = _buildVoice(delay.max.x + marginW, delay.min.y);
auto chorus = _buildChorus(delay.max.x + marginWSec, delay.min.y);

auto lfo2 = _buildLFO!(Params.lfo2Dest - Params.lfo1Dest)(
"LFO2", voice.max.x + marginW, voice.min.y);
auto lfo1 = _buildLFO!(cast(Params) 0)(
"LFO1", lfo2.min.x, eq.min.y);
"LFO1", osc.min.x, osc.max.y + marginH);

auto lfo2 = _buildLFO!(Params.lfo2Dest - Params.lfo1Dest)(
"LFO2", lfo1.max.x + marginWSec, lfo1.min.y);

auto voice = _buildVoice(lfo2.max.x + marginWSec, lfo2.min.y);

addChild(_resizerHint = mallocNew!UIWindowResizer(this.context()));

Expand Down Expand Up @@ -218,12 +224,23 @@ class Synth2GUI : PBRBackgroundGUI!(png1, png2, png3, png3, png3, ""), IParamete
_poly.text(cast(string) _polyStr[]);
}

void setChorusMulti(int multi) {
snprintf(_chorusMultiStr.ptr, _chorusMultiStr.length, "%d", multi);
_chorusMulti.text(cast(string) _chorusMultiStr[]);
}

// TODO: create a new UILabel with IParameterListner for IntegerParameter.
void onParameterChanged(Parameter sender) {
if (sender.index == Params.voicePoly) {
if (auto polyParam = cast(IntegerParameter) sender) {
setPoly(polyParam.value);
}
}
if (sender.index == Params.chorusMulti) {
if (auto polyParam = cast(IntegerParameter) sender) {
setChorusMulti(polyParam.value);
}
}
}

void onBeginParameterEdit(Parameter sender) {}
Expand All @@ -234,6 +251,59 @@ private:

auto _param(Params id)() { return typedParam!id(_params); }

box2i _buildChorus(int x, int y) {
auto label = _addLabel("Chorus", x, y, fontMedium);
auto on = _buildSwitch(
_param!(Params.chorusOn),
rectangle(x, label.position.max.y + marginH, knobRad, knobRad),
"ON");

auto multi = _buildSlider(
_param!(Params.chorusMulti),
rectangle(on.max.x + marginW, on.min.y, slideWidth, slideHeight / 3),
"", []);
_chorusMulti = _addLabel("1", multi.max.x, multi.min.y, fontLarge);
auto multiLabel = _addLabel("multi",
_chorusMulti.position.min.x,
_chorusMulti.position.max.y + marginH,
fontSmall);
_chorusMulti.width = multiLabel.width;

auto chorusTime = _buildKnob(
_param!(Params.chorusTime),
rectangle(x, on.max.y + marginH, knobRad, knobRad),
"time");
auto chorusDepth = _buildKnob(
_param!(Params.chorusDepth),
rectangle(chorusTime.max.x + marginW,
on.max.y + marginH, knobRad, knobRad),
"deph");
auto chorusRate = _buildKnob(
_param!(Params.chorusRate),
rectangle(chorusDepth.max.x + marginH,
on.max.y + marginH, knobRad, knobRad),
"rate");

auto chorusFeedback = _buildKnob(
_param!(Params.chorusFeedback),
rectangle(x, chorusTime.max.y + marginH, knobRad, knobRad),
"fdbk");
auto chorusLevel = _buildKnob(
_param!(Params.chorusLevel),
rectangle(chorusFeedback.max.x + marginW,
chorusTime.max.y + marginH, knobRad, knobRad),
"levl");
auto chorusWidth = _buildKnob(
_param!(Params.chorusWidth),
rectangle(chorusLevel.max.x + marginW,
chorusTime.max.y + marginH, knobRad, knobRad),
"widh");
return expand(label.position, on,
_chorusMulti.position, multiLabel.position,
chorusTime, chorusDepth, chorusRate,
chorusFeedback, chorusLevel, chorusWidth);
}

box2i _buildDelay(int x, int y) {
auto label = _addLabel("Delay", x, y, fontMedium);
auto kind = _buildSlider(
Expand Down Expand Up @@ -686,8 +756,8 @@ private:
UILabel _tempo, _synth2, _date;
char[10] _tempoStr;
double _tempoValue;
UILabel _poly;
char[3] _polyStr;
UILabel _poly, _chorusMulti;
char[3] _polyStr, _chorusMultiStr;
Parameter[] _params;
UIWindowResizer _resizerHint;
Vec!box2i _defaultRects;
Expand Down
1 change: 1 addition & 0 deletions source/synth2/lfo.d
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ struct LFO {
/// tinfo = info on bpm etc.
void setParams(Waveform waveform, bool sync, float normalizedSpeed,
Multiplier mult, TimeInfo tinfo) pure {
// TODO: create separated functions for sync and non-sync.
_wave.waveform = waveform;
if (sync) {
_wave.freq = 1f / Interval(normalizedSpeed.toBar, mult).toSeconds(tinfo.tempo);
Expand Down
Loading

0 comments on commit c3d06e8

Please sign in to comment.