This example demonstrates fundamental audio synthesis concepts in JUCE. It creates a simple polyphonic synthesizer that generates sine waves in response to MIDI input or on-screen keyboard interaction.
- Understand real-time audio processing in JUCE
- Learn MIDI input handling and note management
- Implement polyphonic voice management
- Create interactive audio applications
- Master audio callback optimization
03-basic-synthesizer/
├── README.md # This documentation
├── CMakeLists.txt # Build configuration
├── Source/
│ ├── Main.cpp # Application entry point
│ ├── MainComponent.h # Main GUI and audio component
│ ├── SineWaveVoice.h # Individual synthesizer voice
│ └── SineWaveSound.h # Sound definition for voices
└── build/ # Build directory (created when building)
cd examples/03-basic-synthesizer
mkdir build && cd build
cmake ..
cmake --build . --config Release
# Run the application
# Windows: ./Release/BasicSynthesizer.exe
# macOS: open ./Release/BasicSynthesizer.app
# Linux: ./BasicSynthesizerWhen you run the synthesizer, you'll see:
- On-Screen Keyboard: Click keys to play notes
- Volume Control: Adjust overall output level
- MIDI Input: Connect a MIDI keyboard for real input
- Polyphonic Playback: Play multiple notes simultaneously
- Real-Time Audio: Low-latency audio generation
void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override
{
// Clear the buffer
bufferToFill.clearActiveBufferRegion();
// Let the synthesizer fill the buffer
synthesizer.renderNextBlock(*bufferToFill.buffer,
midiMessages,
bufferToFill.startSample,
bufferToFill.numSamples);
// Apply master volume
bufferToFill.buffer->applyGain(masterVolume);
}void handleNoteOn(juce::MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override
{
// Convert MIDI note to synthesizer message
auto message = juce::MidiMessage::noteOn(midiChannel, midiNoteNumber, velocity);
midiMessages.addEvent(message, 0);
}class SineWaveVoice : public juce::SynthesiserVoice
{
public:
bool canPlaySound(juce::SynthesiserSound* sound) override;
void startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) override;
void stopNote(float velocity, bool allowTailOff) override;
void renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override;
private:
double currentAngle = 0.0;
double angleDelta = 0.0;
double level = 0.0;
double tailOff = 0.0;
};void renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
{
if (angleDelta != 0.0)
{
if (tailOff > 0.0) // Note is releasing
{
while (--numSamples >= 0)
{
auto currentSample = (float)(std::sin(currentAngle) * level * tailOff);
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample(i, startSample, currentSample);
currentAngle += angleDelta;
++startSample;
tailOff *= 0.99; // Exponential decay
if (tailOff <= 0.005)
{
clearCurrentNote();
angleDelta = 0.0;
break;
}
}
}
else // Note is playing
{
while (--numSamples >= 0)
{
auto currentSample = (float)(std::sin(currentAngle) * level);
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample(i, startSample, currentSample);
currentAngle += angleDelta;
++startSample;
}
}
}
}- Mouse Click: Click piano keys to play notes
- Multiple Notes: Hold multiple keys for chords
- Visual Feedback: Keys highlight when pressed
- External Keyboards: Connect USB/MIDI keyboards
- Real-Time Response: Low-latency note triggering
- Velocity Sensitivity: Responds to key velocity
- Master Volume: Overall output level control
- Real-Time Adjustment: Changes apply immediately
- Safe Range: Prevents audio clipping
MIDI Input → Note Events → Voice Allocation → Audio Generation → Output Buffer
↓ ↓ ↓ ↓ ↓
Keyboard/GUI → MidiBuffer → SynthesiserVoice → Sin Wave → Audio Device
- Note On: Allocate voice, set frequency and amplitude
- Sustain: Generate continuous sine wave
- Note Off: Begin exponential decay (tail-off)
- Release: Voice becomes available for reuse
- Voice Pooling: Reuse voice objects to avoid allocation
- Lock-Free: Audio thread never blocks on GUI thread
- Efficient Math: Optimized trigonometric calculations
- Buffer Management: Minimize memory allocations
- Change Waveform: Replace sine wave with sawtooth or square
- Adjust Decay: Modify the tail-off rate (0.99 factor)
- Add Vibrato: Modulate frequency with LFO
- Volume Envelope: Add attack/decay/sustain/release
- Multiple Oscillators: Layer different waveforms
- Filter: Add low-pass or high-pass filtering
- Effects: Add reverb or delay
- Preset System: Save and load different sounds
- Wavetable Synthesis: Use pre-computed waveforms
- FM Synthesis: Implement frequency modulation
- Granular Synthesis: Sample-based synthesis
- Plugin Version: Convert to VST3/AU plugin
- Complete 01-hello-juce for basic JUCE concepts
- Understand basic trigonometry (sine waves)
- Familiarity with MIDI concepts
- 05-audio-effects - Add effects processing
- 06-midi-processor - Advanced MIDI handling
- 07-plugin-template - Convert to plugin
- Check audio device selection in system settings
- Verify sample rate compatibility (44.1kHz/48kHz)
- Ensure audio permissions are granted
- Test with system audio applications first
- Increase audio buffer size (512-1024 samples)
- Reduce number of simultaneous voices
- Optimize mathematical calculations
- Profile code to find bottlenecks
- Connect MIDI device before starting application
- Check MIDI device drivers and system recognition
- Verify MIDI permissions (macOS/iOS)
- Test MIDI device with other applications
- Digital Signal Processing - DSP fundamentals
- MIDI Specification - MIDI protocol details
- Audio Programming Book - Comprehensive guide
- AudioSource - Audio processing base class
- Synthesiser - Polyphonic synthesizer
- MidiKeyboardComponent - On-screen keyboard
This example demonstrates real-time audio synthesis and MIDI handling in JUCE. It serves as a foundation for more complex audio applications and synthesizer development.