ARA2 Waveform Overlay - Implementation Plan¶
Status: Planning
Last updated: 2026-07-08
Goal¶
Display the host DAW's audio waveform behind the pitch curves in the Live visualizer when running in ARA mode (Studio One, Cubase, REAPER, etc.).
Current State¶
PitchVisualizer::setWaveformOverlay(samples, numSamples, sampleRate)is called fromPluginEditor::timerCallback()(guarded by theshowWaveformhamburger-menu toggle) and forwarded to both the visualizer and the curve editor.PitchVisualizer::paintWaveformOverlay()renders the waveform correctly when data is providedAudioProcessorARAExtensionis inherited by the processorARADocumentControllerSpecialisationis implemented (minimal)- The cached waveform is a mono downmix of the input audio captured in
processBlock()(all modes), NOT data read from an ARA content reader. The ARA content-reader extraction path described below is not yet implemented/used.
JUCE ARA2 Audio Access Pattern¶
In JUCE's ARA2 integration, audio data is accessed through:
Processor (AudioProcessorARAExtension)
-> getPlaybackRegions()
-> each ARAPlaybackRegion has:
-> getAudioSource() -> getSampleRate(), getChannelCount(), getFrameCount()
-> getContentReaderForDefinition(ARAContentDefinition, startSample, sampleCount)
-> ARAContentReader with readAudioData() method
Implementation Steps¶
Step 1: Store Waveform Data in Processor (PluginProcessor.h/.cpp)¶
The members already exist in OpenVoxTunerAudioProcessor (implemented):
// ARA waveform cache (actually a mono downmix of input audio)
juce::AudioBuffer<float> araWaveformBuffer;
double araWaveformSampleRate = 44100.0;
bool araWaveformReady = false;
juce::CriticalSection araWaveformLock;
processBlock() (see Step 2),
and exposed to the editor via copyAraWaveform() / isAraWaveformReady().
Step 2: Capture Mono Downmix in processBlock (PluginProcessor.cpp)¶
The current implementation does not use an ARA content reader. Instead,
early in processBlock() (before DSP modifies the buffer, in all modes), it
captures a mono downmix of the input audio:
- Read the input buffer's channels and sample count
- Copy channel 0 into
araWaveformBuffer(sized to 1 channel) - Add the remaining channels and apply an equal gain (
1.0 / numCh) to downmix - Store
currentSampleRateasaraWaveformSampleRate - Set
araWaveformReady = true
Under lock:
const int numSamples = buffer.getNumSamples();
const int numCh = buffer.getNumChannels();
if (numSamples > 0 && numCh > 0)
{
const juce::CriticalSection::ScopedLockType sl (araWaveformLock);
araWaveformBuffer.setSize (1, numSamples, false, false, true);
araWaveformBuffer.copyFrom (0, 0, buffer, 0, 0, numSamples);
for (int ch = 1; ch < numCh; ++ch)
araWaveformBuffer.addFrom (0, 0, buffer, ch, 0, numSamples);
araWaveformBuffer.applyGain (0, 0, numSamples, 1.0f / (float) numCh);
araWaveformSampleRate = currentSampleRate;
araWaveformReady = true;
}
Note: ARA content-reader extraction (
getPlaybackRegions()/createContentReaderForDefinition) is not yet used. If you want the overlay to reflect the DAW's clip data (rather than live input), that path remains future work.
Step 3: Forward to Visualizer in Editor (PluginEditor.cpp)¶
In timerCallback(), when the hamburger "Show Waveform" (showWaveform) is
enabled, copy the cached buffer and forward it to both editors:
juce::AudioBuffer<float> waveform;
double sr = 44100.0;
processorRef.copyAraWaveform (waveform, sr);
if (waveform.getNumSamples() > 0)
{
const float* data = waveform.getReadPointer (0);
if (pitchVisualizer != nullptr)
pitchVisualizer->setWaveformOverlay (data, waveform.getNumSamples(), sr);
if (curveEditor != nullptr)
curveEditor->setWaveformOverlay (data, waveform.getNumSamples(), sr);
}
Step 4: Render in Visualizer (PitchVisualizer.cpp)¶
The existing paintWaveformOverlay() already handles rendering. The waveform is drawn as a light blue semi-transparent fill behind the pitch curves.
Additional improvements: - Map waveform samples to the visualizer's time/pitch coordinate system - Use the ARA playback region's time range to align the waveform with the visualizer's scroll position - Handle multiple regions (concatenated or overlaid)
Step 5: Time Alignment¶
The visualizer scrolls based on transport time (from setPlayheadTime()). The waveform must be aligned to the same timeline:
- Each ARAPlaybackRegion has getLocationStart() / getLocationEnd() (in playback frames)
- Convert these to the visualizer's x-axis coordinate system
- Only render the visible portion of the waveform
Files to Modify¶
| File | Changes |
|---|---|
PluginProcessor.h |
Waveform cache members + lock (already present) |
PluginProcessor.cpp |
Capture mono downmix in processBlock() (already present); ARA content-reader extraction not yet used |
PluginEditor.cpp |
Forward waveform data to visualizer/curve editor in timerCallback() (already present) |
PitchVisualizer.h |
Update setWaveformOverlay() signature (time alignment) |
PitchVisualizer.cpp |
Improve paintWaveformOverlay() for ARA time alignment |
Risks and Mitigations¶
-
Performance: Reading ARA content on every processBlock is expensive.
Mitigation: Read once per region change, cache the result. Only re-read when the region boundary changes. -
DAW compatibility: Not all DAWs expose audio through ARA content readers.
Mitigation: CheckisBoundToARA()before attempting. Graceful fallback to pitch-only display. -
Thread safety: ARA content readers may not be real-time safe.
Mitigation: Read on a background thread, use double-buffering with lock. -
Multiple regions: Some projects have many overlapping regions.
Mitigation: For MVP, only display the first/primary region's waveform.
Testing Plan¶
- Test in Studio One with a single vocal clip
- Verify waveform alignment with pitch curves
- Test with multiple regions (should show primary only)
- Test in non-ARA mode (should show nothing, no crash)
- Performance profiling: ensure <1ms overhead in processBlock