VST Plugin Development for Beginners: Your First Audio Plugin
A beginner-friendly guide to building your first VST audio plugin. Learn the fundamentals of digital audio, plugin architecture, and how to create a simple gain plugin from scratch.
Building VST plugins is one of the most rewarding areas of software development. You create tools that musicians use to make art. This guide will take you from zero to your first working plugin.
As someone who transitioned from Berklee to software engineering, I've found audio plugin development to be the perfect blend of my two passions. Once you're ready for more advanced topics, check out my complete JUCE guide.
What is a VST Plugin?
VST (Virtual Studio Technology) is a standard for audio plugins developed by Steinberg. VST plugins process or generate audio within a DAW (Digital Audio Workstation) like Ableton Live, FL Studio, or Logic Pro.
There are three types:
- VST Instruments (VSTi) - Generate sound (synths, samplers)
- VST Effects - Process audio (EQ, compression, reverb)
- VST MIDI - Process MIDI data
Prerequisites
Before diving in, you'll need:
- C++ basics - Variables, functions, classes, pointers
- A DAW - For testing (Reaper is free to try)
- JUCE Framework - Download from juce.com
- IDE - Xcode (Mac), Visual Studio (Windows)
Don't worry if your C++ is rusty - audio programming is a great way to learn.
Understanding Digital Audio
Audio in computers is represented as samples - numbers that represent the amplitude of a sound wave at a moment in time.
Sample Rate: How many samples per second
- 44,100 Hz (CD quality)
- 48,000 Hz (Video standard)
- 96,000 Hz (High resolution)
Bit Depth: Precision of each sample
- 16-bit: 65,536 possible values
- 24-bit: 16.7 million values
- 32-bit float: Used internally for processing
When you process audio, you're literally doing math on these numbers.
Plugin Architecture
Every audio plugin has two parts:
1. The Processor
Where audio gets processed. This code runs on the audio thread - a high-priority thread that must never block or allocate memory.
void processBlock(AudioBuffer<float>& buffer, MidiBuffer& midi)
{
// This runs thousands of times per second
// Process audio here
}
2. The Editor
The visual interface users see. This runs on the GUI thread - separate from audio processing.
void paint(Graphics& g)
{
// Draw your interface here
}
Critical rule: The audio thread and GUI thread must communicate carefully to avoid glitches.
Your First Plugin: Simple Gain
Let's build a gain plugin - it makes audio louder or quieter.
Step 1: Create the Project
- Open JUCE's Projucer
- Create new project → Audio Plugin
- Name it "SimpleGain"
- Enable VST3 and AU (Mac) formats
- Open in your IDE
Step 2: Add a Parameter
In PluginProcessor.h:
class SimpleGainProcessor : public juce::AudioProcessor
{
public:
// ... constructor and methods ...
juce::AudioProcessorValueTreeState parameters;
private:
std::atomic<float>* gainParameter = nullptr;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SimpleGainProcessor)
};
In PluginProcessor.cpp constructor:
SimpleGainProcessor::SimpleGainProcessor()
: AudioProcessor(BusesProperties()
.withInput("Input", juce::AudioChannelSet::stereo(), true)
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
parameters(*this, nullptr, "Parameters",
{
std::make_unique<juce::AudioParameterFloat>(
"gain", // Parameter ID
"Gain", // Parameter name
0.0f, // Minimum value
2.0f, // Maximum value
1.0f // Default value
)
})
{
gainParameter = parameters.getRawParameterValue("gain");
}
Step 3: Process Audio
In processBlock:
void SimpleGainProcessor::processBlock(
juce::AudioBuffer<float>& buffer,
juce::MidiBuffer& midiMessages)
{
// Get the current gain value (thread-safe)
const float gain = gainParameter->load();
// Process each channel
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
{
// Get pointer to channel's samples
float* channelData = buffer.getWritePointer(channel);
// Apply gain to each sample
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
{
channelData[sample] *= gain;
}
}
}
Step 4: Create the UI
In PluginEditor.h:
class SimpleGainEditor : public juce::AudioProcessorEditor
{
public:
SimpleGainEditor(SimpleGainProcessor&);
~SimpleGainEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
private:
SimpleGainProcessor& processor;
juce::Slider gainSlider;
juce::Label gainLabel;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>
gainAttachment;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SimpleGainEditor)
};
In PluginEditor.cpp:
SimpleGainEditor::SimpleGainEditor(SimpleGainProcessor& p)
: AudioProcessorEditor(&p), processor(p)
{
// Configure the slider
gainSlider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag);
gainSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 80, 20);
addAndMakeVisible(gainSlider);
// Configure the label
gainLabel.setText("Gain", juce::dontSendNotification);
gainLabel.setJustificationType(juce::Justification::centred);
addAndMakeVisible(gainLabel);
// Connect slider to parameter
gainAttachment = std::make_unique<
juce::AudioProcessorValueTreeState::SliderAttachment>(
processor.parameters, "gain", gainSlider);
// Set plugin window size
setSize(200, 250);
}
void SimpleGainEditor::paint(juce::Graphics& g)
{
g.fillAll(juce::Colours::darkgrey);
}
void SimpleGainEditor::resized()
{
auto bounds = getLocalBounds().reduced(20);
gainLabel.setBounds(bounds.removeFromTop(30));
gainSlider.setBounds(bounds);
}
Step 5: Build and Test
- Build the project in your IDE
- Copy the VST3 file to your plugins folder
- Open your DAW and scan for new plugins
- Load SimpleGain on a track
- Play audio and turn the knob!
Common Beginner Mistakes
1. Allocating Memory in processBlock
// BAD - allocates memory on audio thread
void processBlock(AudioBuffer<float>& buffer, MidiBuffer&)
{
std::vector<float> temp(buffer.getNumSamples()); // DON'T DO THIS
}
// GOOD - pre-allocate in prepareToPlay
void prepareToPlay(double sampleRate, int samplesPerBlock)
{
tempBuffer.resize(samplesPerBlock); // Allocate here
}
2. Ignoring Sample Rate
// BAD - assumes sample rate
float filterCutoff = 1000.0f; // This will sound different at 44.1k vs 96k
// GOOD - calculate based on sample rate
void prepareToPlay(double sampleRate, int samplesPerBlock)
{
float normalizedCutoff = cutoffHz / sampleRate;
// Use normalizedCutoff in filter calculations
}
3. Not Handling Different Buffer Sizes
// Your plugin might receive:
// - 64 samples (low latency)
// - 512 samples (normal)
// - 2048 samples (offline render)
// Never assume a fixed size!
Next Steps
Once you've built a gain plugin:
- Add more parameters - Try adding a pan control
- Learn DSP basics - Filters, oscillators, envelopes
- Study existing plugins - Many open-source plugins on GitHub
- Read the JUCE tutorials - Excellent documentation
Resources
- JUCE Documentation
- The Audio Programmer
- Designing Audio Effect Plugins in C++ - Book by Will Pirkle
- My JUCE guide for more advanced topics
Conclusion
Building audio plugins combines creativity with technical skill. Start simple, understand the fundamentals, and gradually add complexity.
I built my first plugin as a simple gain like this one. Now I develop professional audio software at Malinow Audio. Every expert started exactly where you are now.
The audio development community is incredibly welcoming. Don't be afraid to ask questions, share your work, and learn from others.
Happy coding, and may your plugins never clip!
Check out my portfolio to see my audio projects, and browse more blog posts for development tips.
Related Articles: