Roland Rabien https://rabiensoftware.com Audio Software Developer: macOS iOS Windows Linux Sun, 24 Sep 2023 02:07:20 +0000 en-CA hourly 1 https://wordpress.org/?v=6.9.4 Creating an Effect plugin with Gin https://rabiensoftware.com/index.php/2023/09/23/creating-an-effect-plugin-with-gin/ https://rabiensoftware.com/index.php/2023/09/23/creating-an-effect-plugin-with-gin/#respond Sat, 23 Sep 2023 23:33:06 +0000 https://rabiensoftware.com/?p=99 Read more]]> 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.

]]>
https://rabiensoftware.com/index.php/2023/09/23/creating-an-effect-plugin-with-gin/feed/ 0
Gin & Juce https://rabiensoftware.com/index.php/2023/09/09/gin-juce/ https://rabiensoftware.com/index.php/2023/09/09/gin-juce/#respond Sat, 09 Sep 2023 13:48:02 +0000 https://rabiensoftware.com/?p=75 Read more]]> Introduction

Over the past 13 years as an independent contractor I’ve worked on JUCE based projects for companies like Intel, reFX, QSC, Neyrinck Audio and Tracktion. During this time I’ve had to solve the same problems multiple times, so I came up with Gin, a collection of classes and utilities that compliment JUCE and allow me to work faster. Some parts of Gin have been well tested widely deployed in popular plugins, others are for my personal projects or ideas that went nowhere. Unfinished classes are marked with a comment, but feel free to ask me if something is ‘production ready’ before you use it. The following sections will cover some of what’s in Gin and why you might want to use it. This is not a complete list, but just the highlights, please see the online documentation for a full list.

Licensing

I’m not a lawyer or a master of Open Source licenses, but the intention is that for anybody who has a license to use JUCE (either GPL3 or Commercial) and also use Gin free or charge and without restrictions. Therefore I’ve released Gin under the BSD license. As parts of Gin are derivates of JUCE, you also need to abide by JUCE’s license terms. Gin also includes 3rd party code (either BSD, MIT or similar license). I have left the authors original copyright notifications in these files.

While I do not require your changes to be shared back to the main branch, it is appreciated. If you fixed bugs or have new features, please open a pull request.

Example Code

The Gin repository include a Demo Application that uses as many classes as possible with visual demos. There is also a Synth Demo that is a basic subtractive synth.

gin

The core gin module only depends on juce_core and juce_data_structures and is designed to be used from command line apps as well a GUI apps and plugins.

Catenary

A catenary is the shape a wire, cable, rope etc. makes when it hangs. This class is useful for drawing wire connections in a modular synth. Note that in this class the y axis inverted compared to the y axis in juce::Graphics so you will need to invert the y coordinates.

Ellipse

This class provides a set of functions to fine where lines intersect ellipses, points on ellipses, points inside and outside ellipses. Along with Catenary it can be useful for drawing wires with rounded ends.

DownloadManager

DownloadManager is useful for downloading multiple files in the background. Maximum number of concurrent downloads and retry limits can be set. Supports gzip compressed downloads, which JUCE doesn’t automatically support on Windows.

EquationParser

EquationParser takes strings like “6 * 3 + 5” and calculates the resulting value. Supports variables, constants, and functions. Parsing is cached and stored in byte code, so it is very fast if only variables change between evaluations. Based on muParser.

FileSystemWatcher

FileSystemWatcher monitors the file system for changes to files, much more efficient than polling for changes. Useful for file browsers to know when to refresh or watching for new presets and samples in a plugin. Supports Windows, macOS & Linux.

RIFFParser

RIFFParser parses files like WAV and AVI that are based on the RIFF format. Useful for getting metadata chunks that aren’t supported by the JUCE WavAudioFormat. Used internally by the free function gin::getWavetableSize().

gin_dsp

gin_dsp is split into DSP algorithms and Components which are used to display the output of or control those algorithms.

ADSR

A simple linear ADSR, best used as a modulation source.

AnalogADSR

A analog style ADSR based on the algorithm from Will Pirkle’s book. Much better for controlling auto levels than the other ADSR.

AudioEquationParser

Similar to the gin::EquationParser, this subclass adds audio related functions. And since these functions have state, they aren’t true functions without side effects anymore. However, it does let the user type things like (hp12(inL, 5000, 0.707) + hp12(inR, 5000, 0.707)) and it will filter the left and right channels and add them together. Since there is state, make sure you have a separate gin::AudioEquationParser for the left and right channel. For an example of how this can be used, see the Maths plugin.

AudioFifo

A simple wrapper around juce::AbstractFifo, that lets you push and pop from a juce::AudioSampleBuffer.

BandLimitedLookupTable

A collection of lookup tables for an oscillator, with a different lookup table for every few notes. This allows the oscillator to play back notes without aliasing. The tables can either be created from functions like saw, triangle, since, square and pulse. Or they can be loaded from a single cycle wave file, and then the higher frequencies will be removed with an FFT. These tables are used by the gin::StereoOscillator ∫and gin::WTOscillator.

DelayLine

Simple delay with multiple tabs. Either linear or Lagrange interpolation.

Dynamics

Compressor, limiter, expander and gate based on Will Pirkle’s book.

EQ

Bundles up a number of filters to make it easy to have multi channel, multi band EQ.

Filter

Bundles up a number of SVF filters to have multichannel filters with either 12 or 24 dB slope.

GateEffect

Tracegate style effect

LFO

Oscillator with 17 different waveforms, great as a modulation source.

MidiFifo

If your plug-in has latency, and you need to keep the midi in sync with the audio, then a gin::MidiFifo will do just that.

Modulation

Audio modulation, for a chorus or phaser etc

StereoOscillator

Audio rate oscillator with sine, saw, ramp, square, pulse and noise waves. Uses gin::BandLimitedLookupTables to avoid aliasing.

VoicedStereoOscillator

A template class that can take an Oscillator and add multiple voices, detune and spread.

PlateReverb

An implementation of the classic plate reverb algorithm described by Jon Dattorro.

ResamplingFifo

An audio fifo where you push ay one sample rate and pop at another. Uses SecretRabbitCode internally.

SampleOscillator

Not really an oscillator, but it plays audio files at various pitches, so it can be used to create a sample based instrument.

Synthesiser

A subclass of juce::MPESynthesiser but with much better voice handling. Adds fast kill support so there are no clicks when you run out of voices. Supports mono, legato, glissando and portamento. Number of voices can be changed with just a parameter, rather than having to construct and delete Voice objects.

ValueSmoother

A value smoother that only works on values between 0.0 and 1.0, but smooths at a fix rate, rather than a fixed time from current value to new value.

WTOscillator

Wave table oscillator with formant and bend phase distortion. Uses gin::BandLimitedLookupTables to avoid aliasing.

TriggeredScope

Triggered Scope that it can be set to start on a rising or falling signal. This makes it extremely useful for very zoomed-in waveform viewing.

WavetableComponent

Draws a sweet 3D looking wavetable.

gin_graphics & gin_webp

The gin_graphics module extends the juce_graphics module with 2 new image file formats: webp and bmp.

There are also several free functions for applying image effect to juce::Image including vignette, sepia, soften, sharpen, game, contrast, brightness, stack blur, higher quality AVIR resampling, Photoshop inspired blend modes.

gin_gui

The gin_gui module extends juce_gui_basics with the following new classes:

ComponentViewer

 

A useful tool for debugging, however the mouse over components to get the name, class, size, hierarchy, etc. Superseded by Melatonin Inspector.

MapViewer

Display an OpenStreetMap zoomable and scrollable map.

FilePropertyComponent / ColourPropertyComponent

juce::PropertyComponent subclasses for File and Colour

CoalescedTimer

A way to combine juce::Timers to ensure all timers with the same rate trigger at the same time. Usually for making sure all Component update from the same 60 Hz timer for example.

ElevatedFileCopy

For macOS and Windows, all the user to copy files into folders that require admin access.

Layout

This class probably deserves it’s own article. Allows Component layouts to be set from json, supports hot reloading to build layouts at runtime. Component positions are set by name, x, y, width and height. Functions are available to get the positions of other components. gin::EquationParser is used internally so mathematical expressions are supported. Square brackets can be used to layout several components at once.

The following code does the layout for components name lfo1, lfo2 and lfo3. The y position for lfo1 is prevB()+1 while lfo2 and lfo3 use prevY() for their y position. All other coordinates are the same for each. If you are using this class with gin_plugin, each knob, select or switch component will be named after the short name of the parameter it is controlling.

{ "id": "lfo[1..3]", "x": 0, "y": "prevB()+1,prevY()", "w": 280, "h": 163, "children":
  [
    { "id": "Sync", "x": 0, "y": 23, "w": 56, "h": 70 },
    { "id": "Beat", "x": "prevR()", "y": 23, "w": 56, "h": 70 },
    { "id": "Rate", "x": "prevX()", "y": 23, "w": 56, "h": 70 },
  ] 
},

For full examples see Organ or Wavetable.

AsyncDownload

Downloads on a background thread, but until the similar juce functionality, doesn’t do the DNS lookup on the main thread, which can lock up your UI.

gin_location

This is a work in process module for getting the current location from the device GPS. It is nowhere near ready for use.

gin_metadata

Loads EXIF, IPTC, XMP, and comment metadata from JPEG and PNG images. If you are displaying a JPEG that came from a camera, you’ll need to get the orientation metadata to ensure the image is displayed the correct way up. GPS location, focal length, aperture, ISO may also be useful to know.

gin_network

SecureStreamingSocket

A drop in replacement for juce::StreamingSocket but it supports SSL.

WebSocket

Webscocket client, supports HTTPS thanks to gin::SecureStreamingSocket

gin_plugin

This module really needs an article on it’s own. It provides subclasses of juce::AudioProcessor, juce::AudioProcessorParameter and juce::AudioProcessorEditor that make it much faster and easier to get started writing a effect or synth plugin. Preset loading and saving is handled. The editor has a titlebar for loading / saving presets and a patch browser. The plugin editor layout can either be done via json or snapped to a large grid.

See the HugeGain plugin for a simple example, or Wavetable for a full featured synth.

The following code:

PluginEditor::PluginEditor (PluginProcessor& p)
    : gin::ProcessorEditor (p), proc (p)
{
    for (auto pp : p.getPluginParameters())
    {
        ParamComponent* pc;

        if (pp->isOnOff())
             pc = new Switch (pp);
        else
             pc = new Knob (pp);

        addAndMakeVisible (pc);
        controls.add (pc);
     }
     setGridSize (6, 1);
}

void PluginEditor::resized()
{
    gin::ProcessorEditor::resized();

    componentForId (PARAM_GAIN_L)->setBounds (getGridArea (1, 0));
    componentForId (PARAM_GAIN_S)->setBounds (getGridArea (2, 0));
    componentForId (PARAM_GAIN_R)->setBounds (getGridArea (3, 0));
    componentForId (PARAM_CLIP)->setBounds (getGridArea (4, 0));
}

Creates the following UI:

The gin::Parameter class supports optional smoothing to avoid zipper effect. Modulation via gin::ModMatrix.

Setting up the parameters is as follows:

gainl = addExtParam (PARAM_GAIN_L, "Left", "", "dB", {-100.0f, 100.0f, 0.0f, 5.0f}, 0.0f, 0.1f);
gains = addExtParam (PARAM_GAIN_S, "Both", "", "dB", {-100.0f, 100.0f, 0.0f, 5.0f}, 0.0f, 0.1f);
gainr = addExtParam (PARAM_GAIN_R, "Right", "", "dB", {-100.0f, 100.0f, 0.0f, 5.0f}, 0.0f, 0.1f);
clipp = addExtParam (PARAM_CLIP, "Clip", "", "", { 0.0f, 1.0f, 1.0f, 1.0f}, 1.0f, 0.1f, onOffTextFunction);

gainl->conversionFunction = [] (float in) { return Decibels::decibelsToGain (in); };
gains->conversionFunction = [] (float in) { return Decibels::decibelsToGain (in); };
gainr->conversionFunction = [] (float in) { return Decibels::decibelsToGain (in); };

And then the process block function is as follows, doing it sample by sample if smoothing is currently active or by blocks if parameters have changed recently:

void PluginProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer&)
{
    int numSamples = buffer.getNumSamples();

    if (isSmoothing())
    {
        int pos = 0;

        while (pos < numSamples)
        {
            auto workBuffer = sliceBuffer (buffer, pos, 1);

            workBuffer.applyGain (0, 0, 1, gainl->getProcValue (1));
            workBuffer.applyGain (1, 0, 1, gainr->getProcValue (1));
            workBuffer.applyGain (gains->getProcValue (1));

            pos++;
        }
    }
    else
    {
        buffer.applyGain (0, 0, numSamples, gainl->getProcValue (numSamples));
        buffer.applyGain (1, 0, numSamples, gainr->getProcValue (numSamples));
        buffer.applyGain (gains->getProcValue (numSamples));
    }

    if (clipp->getUserValue() != 0.0f)
        clip (buffer, -1.0f, 1.0f);
}

There are also numerous components for the plugin editor including LFO display, envelope display, patch browser, mod matrix, modulation source draggers, step lfo editor. The Wavetable synth uses no custom components, other than gin::ParamBox subclasses to group components, everything else is from Gin.

Screenshot 1

Conclusion

That is a high level overview of the Gin library. In future posts, I’ll give into some of the classes in more detail. If you have any questions or bugs, join the discussion or issues on GitHub or ask on my Discord.

]]>
https://rabiensoftware.com/index.php/2023/09/09/gin-juce/feed/ 0