Getting Started with JUCE — Build Cross-Platform Audio Apps QuicklyJUCE is a widely used C++ framework for building audio applications and plugins that run on Windows, macOS, Linux, iOS, and Android. It provides modules for audio processing, user interfaces, MIDI handling, file I/O, and plugin formats (VST3, AU, AAX), along with tools such as the Projucer for project setup. This guide walks you through what JUCE is, how to set up your development environment, create a simple cross-platform audio application, and points you toward best practices and useful resources.
Why choose JUCE?
- Cross-platform consistency: write code once and compile for multiple desktop and mobile platforms.
- Audio-focused: built-in audio and MIDI classes, real-time safe idioms, and plugin format support.
- Active ecosystem: many tutorials, example projects, and community modules.
- Modern C++: leverages C++11+ patterns and offers a modular structure that’s easy to integrate into larger projects.
Prerequisites
Before you start, make sure you have:
- Basic knowledge of C++ (classes, RAII, smart pointers).
- Familiarity with the concept of audio callbacks and real-time constraints is helpful but not required.
- A development environment for your target platforms:
- Windows: Visual Studio (⁄2022) or MSVC toolchain.
- macOS: Xcode.
- Linux: a recent GCC/Clang, Make/CMake, and an editor or IDE.
- iOS/Android: Xcode and Android Studio for mobile builds.
Installing JUCE
- Download JUCE from the official website or clone the repository:
- Open the Projucer (JUCE’s project management tool) included in the repo, or generate projects with CMake. Note: JUCE increasingly supports CMake as the recommended build method for many workflows.
Project setup: Projucer vs CMake
- Projucer
- GUI for creating and configuring JUCE projects.
- Convenient for quick prototyping and exploring settings.
- Can export platform-specific projects (Xcode, Visual Studio).
- CMake
- Recommended for production and CI because it integrates smoothly with modern toolchains and avoids the Projucer’s generated-project roundtrips.
- JUCE provides CMake targets in the repo; you can include JUCE as a subdirectory or use it via packaged distributions.
Example CMake snippet to add JUCE as a subdirectory:
add_subdirectory(path/to/JUCE) juce_add_gui_app(MyApp PRODUCT_NAME "My JUCE App" SOURCES ${SRC_FILES} ) target_link_libraries(MyApp PRIVATE juce::juce_gui_extra juce::juce_audio_utils)
First app: A minimal audio application
We’ll create a simple audio application that generates a sine wave and displays a basic GUI slider for frequency control. The key parts are the audio processing callback and a GUI component for user interaction.
Project structure:
- Source/
- Main.cpp
- MainComponent.h / MainComponent.cpp
- PluginProcessor equivalents are not required for standalone apps
Main.cpp (entry point for a JUCE GUI app):
#include <JuceHeader.h> #include "MainComponent.h" class SineApplication : public juce::JUCEApplication { public: const juce::String getApplicationName() override { return "JUCE Sine App"; } const juce::String getApplicationVersion() override { return "1.0"; } void initialise (const juce::String&) override { mainWindow.reset (new MainWindow ("JUCE Sine App", new MainComponent(), *this)); } void shutdown() override { mainWindow = nullptr; } class MainWindow : public juce::DocumentWindow { public: MainWindow (juce::String name, juce::Component* c, JUCEApplication& a) : DocumentWindow (name, juce::Colours::lightgrey, DocumentWindow::allButtons), app (a) { setUsingNativeTitleBar (true); setContentOwned (c, true); centreWithSize (getWidth(), getHeight()); setVisible (true); } void closeButtonPressed() override { app.systemRequestedQuit(); } private: JUCEApplication& app; }; private: std::unique_ptr<MainWindow> mainWindow; }; START_JUCE_APPLICATION (SineApplication)
MainComponent.h (component with audio source and slider):
#pragma once #include <JuceHeader.h> class MainComponent : public juce::AudioAppComponent, private juce::Timer { public: MainComponent(); ~MainComponent() override; void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override; void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override; void releaseResources() override; void paint (juce::Graphics& g) override; void resized() override; private: juce::Slider freqSlider; std::atomic<double> frequency { 440.0 }; double sampleRate = 44100.0; double phase = 0.0; void timerCallback() override { /* optional UI updates */ } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent) };
MainComponent.cpp (implementation):
#include "MainComponent.h" MainComponent::MainComponent() { addAndMakeVisible(freqSlider); freqSlider.setRange(20.0, 20000.0, 0.01); freqSlider.setSkewFactorFromMidPoint(440.0); freqSlider.setValue(440.0); freqSlider.onValueChange = [this] { frequency = freqSlider.getValue(); }; setSize (600, 200); setAudioChannels (0, 2); // no inputs, two outputs startTimerHz(30); } MainComponent::~MainComponent() { shutdownAudio(); } void MainComponent::prepareToPlay (int samplesPerBlockExpected, double sr) { sampleRate = sr; } void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) { auto* left = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample); auto* right = bufferToFill.buffer->getNumChannels() > 1 ? bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample) : nullptr; const double twoPi = juce::MathConstants<double>::twoPi; const double currentFreq = frequency.load(); for (int sample = 0; sample < bufferToFill.numSamples; ++sample) { const float value = (float) std::sin (phase); phase += twoPi * currentFreq / sampleRate; if (phase >= twoPi) phase -= twoPi; left[sample] = value; if (right) right[sample] = value; } } void MainComponent::releaseResources() {} void MainComponent::paint (juce::Graphics& g) { g.fillAll (juce::Colours::black); } void MainComponent::resized() { freqSlider.setBounds (10, 10, getWidth() - 20, 40); }
Build and run: export an Xcode/Visual Studio project via Projucer or use CMake to compile. Running the app should produce a sine tone whose frequency you can change with the slider.
Writing plugins with JUCE
The same JUCE modules power audio plugins. The main differences:
- Implement an AudioProcessor subclass (processBlock) rather than AudioAppComponent.
- Use AudioProcessorEditor for the plugin GUI.
- Use the Projucer or CMake to enable plugin formats (VST3/AU/AAX).
- Be real-time safe in processBlock: avoid heap allocations, locks, file I/O, or blocking calls.
Minimal plugin processBlock sketch:
void MyProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) { auto numChannels = buffer.getNumChannels(); for (int ch = 0; ch < numChannels; ++ch) { auto* data = buffer.getWritePointer (ch); for (int i = 0; i < buffer.getNumSamples(); ++i) data[i] = generateSample(); // implement real-time safe generator } }
Best practices
- Real-time safety: avoid memory allocations, locks, and any OS-blocking calls in audio callbacks. Use lock-free FIFO structures for passing large data between threads.
- Use AudioProcessorValueTreeState for parameter management and automation in plugins.
- Separate DSP and GUI code: keep audio-processing logic independent from UI to ease testing and reuse.
- Use SIMD and optimized math libraries for CPU-heavy DSP.
- Profile and test on target platforms, especially mobile devices which have stricter resource limits.
- Prefer CMake for reproducible builds and CI pipelines.
Useful JUCE modules and classes
- juce_audio_basics, juce_audio_formats, juce_audio_processors, juce_audio_utils
- AudioDeviceManager, AudioAppComponent, AudioProcessor, AudioProcessorEditor
- AudioTransportSource, AudioFormatReader, AudioThumbnail, MidiBuffer
- DSP module (juce::dsp namespace) with filters, oscillators, FFT helpers
Debugging tips
- Use juce::Logger::writeToLog for quick non-realtime logging (avoid logging inside audio callbacks).
- Use platform native profilers (Instruments on macOS, Visual Studio Profiler on Windows, Linux perf) for CPU hotspots.
- Validate sample rates and buffer sizes in prepareToPlay.
- Test with different host DAWs for plugins; hosts may call processBlock with varying buffer sizes or on different threads.
Learning resources
- JUCE API documentation and module reference.
- Example projects in the JUCE repo (Audio Plugin Demo, Synthesiser examples).
- Community forums and tutorials (search for specific topics like AudioProcessorValueTreeState, dsp::Oscillator, and plugin hosting).
- Books and courses on audio programming and DSP fundamentals.
Next steps: a small roadmap
- Recreate the sine app and experiment with more controls (gain, LFO, ADSR).
- Move DSP into a separate class and write unit tests for your processing.
- Create a VST3/AU plugin version using JUCE’s plugin project template.
- Optimize CPU and memory usage; add parameter automation.
- Package installers for each platform and test on target machines.
JUCE lets you iterate quickly between prototype and production while keeping cross-platform concerns manageable. Start small, respect real-time constraints, and progressively add features as you learn the framework’s idioms and modules.