Creating an Effect plugin with Gin
Introduction
Creating a plugin with Gin & JUCE is a lot fast than with JUCE alone. A lot a boiler plate is handled for you, which lets you focus on getting your DSP working. This tutorial explains the code in the Gin example Effect plugin which is a simple gain and pan plugin. A basic understanding of JUCE plugins is assumed. Start by forking and renaming the GinPlugin repo. Edit the CMakeLists.txt with you plugin name and ID and the build.sh script with the name of your plugin. The repo is already setup to build a VST3, AU, LV2 plugin on Windows, macOS and Linux. Notarization on macOS is supported by adding your Apple developer account ID and signing keys as repository secrets.
First Steps
Subclass your processor from gin::Processor instead of juce::AudioProcessor and your editor from gin::ProcessorEditor instead of juce::AudioProcessorEditor.
The gin::ProcessorOptions class allows you to specify options for your plugin. By default it is filled with sensible defaults based on various JUCE #defines. At the end of your constructor you also need to call init(). A minimal plugin should look like this:
static gin::ProcessorOptions getOpts() { gin::ProcessorOptions opts; // Fill in your custom options here return opts; } EffectAudioProcessor::EffectAudioProcessor() : gin::Processor (false, getOpts()) { // call init and the end of your constructor init(); }
Gin automatically handles loading and saving state, loading presets etc, all that is requires of your processor is to create the parameters and implement the processBlock function.
Adding Parameters
Gin supports internal parameters and external parameters. Internal parameters are not visible to the host, and should be used for things that have no need to be automated. They are also useful for ranges that might change in size, like number of filter types. External parameters are exposed to the host and can be optionally automatable or not.
For the vol & pan effect we will add 4 parameters, 2 internal (pan law & polarity invert) and 2 external (vol & pan)
levelParam = addExtParam ("level", "Level", {}, "dB", {-100.0f, 5.0f, 0.0f, 5.0f}, 0.0f, 0.05f); panParam = addExtParam ("pan", "Pan", {}, {}, {-1.0f, 1.0f, 0.0f, 1.0}, 0.0f, 0.05f, panTextFunction); modeParam = addIntParam ("mode", "Mode", {}, {}, { 0.0f, 1.0f, 0.0f, 1.0}, 0.0f, 0.0f, modeTextFunction); invertParam = addIntParam ("invert", "Invert", {}, {}, { 0.0f, 1.0f, 0.0f, 1.0}, 0.0f, 0.0f, onOffTextFunction);
For each parameter, provide and id, name, short name (optional), label (optional), range, default, smoothing time (optional) and value to text function (optional). A smoothing time prevents zipper noise when the user adjusts a parameter. Setting a smoothing time of 0 disables smoothing, leaving it up to your algorithm to provide smoothing itself.
Gin parameters have up to 3 values associated with them. A value which is always between 0 and 1. A user value, which is displayed in the user interface and a processing value which is used by the processing algorithm. In some cases all these values may be in the 0 to 1 range, but for the level param they are all different. The user value is in dB and is in the range -100 db to +5 db and the processing value is the gain and is in the range 0 to ~3.1.
To provide a processing value for a parameter, set the gin::Parameter::conversionFunction.
levelParam->conversionFunction = [] (float in) { return juce::Decibels::decibelsToGain (in); };
Text functions convert the user value into a string. If one isn’t provided a simple number with up to 3 decimal places is show. Text functions are used to convert mod and invert into strings and to add L, C or R to the pan value.
static juce::String modeTextFunction (const gin::Parameter&, float v) { if ((int (v)) == 0) return "Linear"; return "3dB"; } static juce::String onOffTextFunction (const gin::Parameter&, float v) { if (int (v) == 0) return "On"; return "Off"; } static juce::String panTextFunction (const gin::Parameter&, float v) { if (juce::String (v, 2) == "0.00") return "C"; if (v < 0) return juce::String (-v, 2) + "L"; return juce::String (v, 2) + "R"; }
DSP Code
The heart of the processing block is this simple function that converts a pan and volume into a left and right gain with two different pan laws.
auto getGains = [] (float gain, float pan, int mode) -> std::pair<float, float> { if (mode == 0) { const float pv = pan * gain; return { gain - pv, gain + pv }; } else { pan = (pan + 1.0f) * 0.5f; return { gain * std::sin ((1.0f - pan) * juce::MathConstants<float>::halfPi), gain * std::sin (pan * juce::MathConstants<float>::halfPi) }; } };
First we will get the parameters that won’t be smoothed and store them in variables. For the example, we will assume the user is ok with clicks if they change the pan law or polarity, but expect pan and volume to adjust without artifacts. Gin parameters can be accessed as ints or bools, which can save some casting.
const auto mode = modeParam->getUserValueInt(); const auto inv = invertParam->getUserValueBool() ? -1.0f : 1.0f; const auto numSamps = buffer.getNumSamples(); auto pos = 0;
If no parameters are currently changing, this function can be applied to the entire block with the same input parameters, otherwise they need to change for each sample. First, we check if any parameters are changing and work sample by sample.
while (isSmoothing() && pos < numSamps) { auto gain = levelParam->getProcValue (1); auto pan = panParam->getProcValue (1); auto [left, right] = getGains (gain, pan, mode); buffer.applyGain (0, pos, 1, left * inv); buffer.applyGain (1, pos, 1, right * inv); pos++; }
The function gin::Parameter::getProcValue(int stepSize) returns the next smoothed processing value for the parameter. The loop will continue until the parameters have reached their destination value or there is no more input to process. If there is still more input to process, it can be handled in a block for efficiency.
if (pos < numSamps) { auto todo = numSamps - pos; auto gain = levelParam->getProcValue (todo); auto pan = panParam->getProcValue (todo); auto [left, right] = getGains (gain, pan, mode); buffer.applyGain (0, pos, todo, left * inv); buffer.applyGain (1, pos, todo, right * inv); }
Creating the Editor
If your plugin has UI (and it should), create it here. If you want your plugin to resize, wrap your editor in a gin::ScaledPluginEditor. Also create the plugin itself.
bool EffectAudioProcessor::hasEditor() const { return true; } juce::AudioProcessorEditor* EffectAudioProcessor::createEditor() { return new EffectAudioProcessorEditor (*this); } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new EffectAudioProcessor(); }
UI Code
Gin uses a grid layout for components. When creating the UI first specify the size of the grid, in this case 6 x 1. And then layout the controls on the grid.
setGridSize (6, 1); addControl (new gin::Select (p.modeParam), 1, 0); addControl (new gin::Knob (p.levelParam), 2, 0); addControl (new gin::Knob (p.panParam, true), 3, 0); addControl (new gin::Switch (p.invertParam), 4, 0);
Gin provides 3 basic controls. Select, based on a ComboBox. Knob, based on a rotary slider, and Switch, based on a toggle button. Add the to the UI with their grid position and Gin will handle updating them when their parameter changes and deleting them when the editor closes.
The UI will look like this. Preset browser, preset load and save are all built in.
Next Steps
If your plugin requires more state than just parameters, it can be saved in juce::ValueTree gin::Processor::state. Override the functions stateUpdated and updateState if you need to copy variables to / from the ValueTree.
Create your own look and feel based on gin::CopperLookAndFeel so that your plugins don’t look like my plugins.
If the grid based layout is too restrictive, see the gin::Layout class to layout your components with json.
If you have presets, add them to the plugin as binary resources. Then extract them in your constructor if the y don’t already exist.
auto sz = 0; for (auto i = 0; i < BinaryData::namedResourceListSize; i++) if (juce::String (BinaryData::originalFilenames[i]).endsWith (".xml")) if (auto data = BinaryData::getNamedResource (BinaryData::namedResourceList[i], sz)) extractProgram (BinaryData::originalFilenames[i], data, sz);
Conclusion
You now have a full featured plugin with almost no code at all.