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