1 module main;
2 
3 import std.math;
4 
5 import dplug.core,
6        dplug.client,
7        dplug.dsp;
8 
9 import gui;
10 import ar;
11 
12 // This define entry points for plugin formats,
13 // depending on which version identifiers are defined.
14 mixin(pluginEntryPoints!DistortClient);
15 
16 enum : int
17 {
18     paramInput,
19     paramDrive,
20     paramBias,
21     paramOutput,
22     paramOnOff,
23 }
24 
25 /**
26     A small distortion plug-in named Distort!
27 
28     It demonstrates:
29         - parameters
30         - I/O settings (mono or stereo)
31         - basic presets
32         - latency reporting
33         - using biquads from dplug:dsp
34         - custom UI widgets and custom DSP
35         - drawing with `dplug:canvas` and `canvasity`
36         - resizeable UI
37         - basic DSP->UI feedback
38         - use of `dplug:pbr-widgets`
39 
40     To go further:
41         - Examples:     ClipIt and Template.
42         - FAQ:          https://dplug.org/tutorials
43         - Inline Doc:   https://dplug.dpldocs.info/dplug.html
44 */
45 
46 final class DistortClient : dplug.client.Client
47 {
48 public:
49 nothrow:
50 @nogc:
51 
52     this()
53     {
54     }
55 
56     ~this()
57     {
58         _inputRMS.reallocBuffer(0);
59         _outputRMS.reallocBuffer(0);
60     }
61 
62     override PluginInfo buildPluginInfo()
63     {
64         // Plugin info is parsed from plugin.json here at compile time.
65         // Indeed it is strongly recommended that you do not fill PluginInfo 
66         // manually, else the information could diverge.
67         static immutable PluginInfo pluginInfo = parsePluginInfo(import("plugin.json"));
68         return pluginInfo;
69     }
70 
71     // This is an optional overload, default is zero parameter.
72     // Caution when adding parameters: always add the indices
73     // in the same order than the parameter enum.
74     override Parameter[] buildParameters()
75     {
76         auto params = makeVec!Parameter();
77         params ~= mallocNew!GainParameter(paramInput, "input", 6.0, 0.0);
78         params ~= mallocNew!LinearFloatParameter(paramDrive, "drive", "%", 0.0f, 100.0f, 20.0f);
79         params ~= mallocNew!LinearFloatParameter(paramBias, "bias", "%", 0.0f, 100.0f, 50.0f);
80         params ~= mallocNew!GainParameter(paramOutput, "output", 6.0, 0.0);
81         params ~= mallocNew!BoolParameter(paramOnOff, "enabled", true);
82         return params.releaseData();
83     }
84 
85     override LegalIO[] buildLegalIO()
86     {
87         auto io = makeVec!LegalIO();
88         io ~= LegalIO(1, 1);
89         io ~= LegalIO(2, 2);
90         return io.releaseData();
91     }
92 
93     // This override is optional, this supports plugin delay compensation in hosts.
94     // By default, 0 samples of latency.
95     override int latencySamples(double sampleRate) pure const 
96     {
97         return 0;
98     }
99 
100     // This override is optional, the default implementation will
101     // have one default preset.
102     override Preset[] buildPresets()
103     {
104         auto presets = makeVec!Preset();
105         presets ~= makeDefaultPreset();
106 
107         static immutable float[] silenceParams = [0.0f, 0.0f, 0.0f, 1.0f, 0];
108         presets ~= mallocNew!Preset("Silence", silenceParams, defaultStateData());
109 
110         static immutable float[] fullOnParams = [1.0f, 1.0f, 0.4f, 1.0f, 0];
111         presets ~= mallocNew!Preset("Full-on", fullOnParams, defaultStateData());
112         return presets.releaseData();
113     }
114 
115     // This override is also optional. It allows to split audio buffers in order to never
116     // exceed some amount of frames at once.
117     // This can be useful as a cheap chunking for parameter smoothing.
118     // Buffer splitting also allows to allocate statically or on the stack with less worries.
119     // It also makes the plugin uses constant memory in case of large buffer sizes.
120     // In VST3, parameter automation gets more precise when this value is small.
121     override int maxFramesInProcess() const
122     {
123         return 512;
124     }
125 
126     override void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs)
127     {
128         // Clear here any state and delay buffers you might have.
129         assert(maxFrames <= 512); // guaranteed by audio buffer splitting
130 
131         _sampleRate = sampleRate;
132 
133         _levelInput.initialize(sampleRate);
134         _levelOutput.initialize(sampleRate);
135 
136         _inputRMS.reallocBuffer(maxFrames);
137         _outputRMS.reallocBuffer(maxFrames);
138 
139         foreach(channel; 0..2)
140         {
141             _hpState[channel].initialize();
142         }
143     }
144 
145     override void processAudio(const(float*)[] inputs, float*[]outputs, int frames, TimeInfo info)
146     {
147         assert(frames <= 512); // guaranteed by audio buffer splitting
148 
149         int numInputs = cast(int)inputs.length;
150         int numOutputs = cast(int)outputs.length;
151         assert(numInputs == numOutputs);
152 
153         const float inputGain = convertDecibelToLinearGain(readParam!float(paramInput));
154         float drive = readParam!float(paramDrive) * 0.01f;
155         float bias = readParam!float(paramBias) * 0.01f;
156         const float outputGain = convertDecibelToLinearGain(readParam!float(paramOutput));
157 
158         const bool enabled = readParam!bool(paramOnOff);
159 
160         if (enabled)
161         {
162             BiquadCoeff highpassCoeff = biquadRBJHighPass(150, _sampleRate, SQRT1_2);
163             for (int chan = 0; chan < numOutputs; ++chan)
164             {
165                 // Distort and put the result in output buffers
166                 for (int f = 0; f < frames; ++f)
167                 {
168                     const float inputSample = inputGain * 2.0 * inputs[chan][f];
169 
170                     // Distort signal
171                     const float distorted = tanh(inputSample * drive * 10.0f + bias) * 0.9f;
172                     outputs[chan][f] = outputGain * distorted;
173                 }
174 
175                 // Highpass to remove bias
176                 _hpState[chan].nextBuffer(outputs[chan], outputs[chan], frames, highpassCoeff);
177             }
178         }
179         else
180         {
181             // Bypass mode
182             for (int chan = 0; chan < numOutputs; ++chan)
183                 outputs[chan][0..frames] = inputs[chan][0..frames];
184         }
185 
186         // Compute feedback for the UI.
187         // We take the first channel (left) and process it, then send it to the widgets.
188         _levelInput.nextBuffer(inputs[0], _inputRMS.ptr, frames);
189         _levelOutput.nextBuffer(outputs[0], _outputRMS.ptr, frames);
190 
191         // Update meters from the audio callback.
192         if (DistortGUI gui = cast(DistortGUI) graphicsAcquire())
193         {
194             gui.sendFeedbackToUI(_inputRMS.ptr, _outputRMS.ptr, frames, _sampleRate);
195             graphicsRelease();
196         }
197     }
198 
199     override IGraphics createGraphics()
200     {
201         return mallocNew!DistortGUI(this);
202     }
203 
204     version(legacyBinState)
205     {}
206     else
207     {
208         /// Important: See documentation in `Client.saveState`.
209         ///            Right now saving extra state is fraught with peril!
210         override void saveState(ref Vec!ubyte chunk)
211         {
212             // dplug.core.binrange allows to write arbitrary chunk bytes here.
213             // You are responsible for versioning, correct UI interaction, etc.
214             //
215             // `loadState` will be called with your own state chunks without regards for
216             // your plugin major version.
217             //
218             // See `saveState` definition in client.d for highly-recommended information.
219             writeLE!uint(chunk, getPublicVersion().major);
220         }
221 
222         /// Important: See documentation in `Client.loadState`.
223         override bool loadState(const(ubyte)[] chunk)
224         {
225             // Parsing is done with error codes.
226             const(ubyte)[] c = chunk;
227             bool err;
228             int major = popLE!uint(c, &err);
229             if (err)
230                 return false;
231 
232             // You're supposed to refuse a chunk that you are not compatible with, with your own
233             // versioning. For example, maybe you break your state chunk compat on plugin majors
234             // versions.
235             if (major != getPublicVersion().major)
236                 return false;
237 
238             return true; // no issue parsing the chunk, and acting on it
239         }
240     }
241 
242 private:
243     LevelComputation _levelInput;
244     LevelComputation _levelOutput;
245     BiquadDelay[2] _hpState;
246     float _sampleRate;
247     float[] _inputRMS, _outputRMS;
248 }
249 
250 
251 /// Sliding dB computation.
252 /// Simple envelope follower, filters the envelope with 24db/oct lowpass.
253 struct LevelComputation
254 {
255 public:
256 nothrow:
257 @nogc:
258 
259     void initialize(float samplerate)
260     {
261         float attackSecs = 0.001;
262         float releaseSecs = 0.001;
263         float initValue = -140;
264         _envelope.initialize(samplerate, attackSecs, releaseSecs, initValue);
265     }
266 
267     // take audio samples, output RMS values.
268     void nextBuffer(const(float)* input, float* output, int frames)
269     {
270         // Compute squared value
271         for(int i = 0; i < frames; ++i)
272             output[i] = input[i] * input[i] + 1e-10f; // avoid -inf
273 
274         // Take log
275         for (int n = 0; n < frames; ++n)
276         {
277             output[n] = convertLinearGainToDecibel(output[n]);
278         }
279 
280         _envelope.nextBuffer(output, output, frames);
281     }
282 
283 private:
284     AttackRelease!float _envelope;
285 }
286 
287 
288