Stereo Correlation

When mixing in stereo, one of the most overlooked but critical checks is stereo correlation — how “in phase” your left and right channels are. This single number tells you whether your mix will hold up when played back in mono. Stereo correlation is essentially the normalized dot product between the left and right channel waveforms. Basically, we measure how similar the two waveforms are in shape and timing over a window of N samples:

(1)   \begin{equation*} \rho = \frac{\sum_n L[n] \cdot R[n]}{\sqrt{\sum_n L[n]^2} \cdot \sqrt{\sum_n R[n]^2}} \end{equation*}

So if at time n:

  • L[n] = instantaneous amplitude (in PCM) of the left channel waveform at sample n
  • R[n] = instantaneous amplitude (in PCM) of the right channel waveform at sample n

 

The stereo correlation formula is mathematically identical to the cosine similarity between two vectors:

(2)   \begin{equation*} \text{cos\_sim}(L, R) = \frac{L \cdot R}{\|L\| \cdot \|R\|} \end{equation*}

  • ρ = +1 → vectors point the same way → signals are perfectly in phase.
  • ρ = 0 → vectors are orthogonal → signals are unrelated.
  • ρ = -1 → vectors point in opposite directions → signals are perfectly out of phase.

 

What This Means Musically

  • Low frequencies (bass, kick, subs): Long wavelengths are prone to cancellation. Even small phase shifts can wipe out the low end when summed to mono. That’s why engineers often keep the bass mono.
  • Mids (vocals, guitars, snares): Phase differences cause comb filtering — certain frequencies cancel, making vocals sound thin or hollow.
  • Highs (cymbals, reverb, shimmer): Short wavelengths phase out quickly. Stereo wideners and reverbs may sparkle in stereo, but collapse to dullness in mono.

 

float calculateStereoCorrelation(const juce::AudioBuffer<float>& buffer)
{
    const int numSamples = buffer.getNumSamples();
    const int numChannels = buffer.getNumChannels();
    
    if (numChannels < 2 || numSamples == 0) return 0.0f;
        
    const float* left = buffer.getReadPointer(0);
    const float* right = buffer.getReadPointer(1);

    double sumLR = 0.0;
    double sumLL = 0.0;
    double sumRR = 0.0;

    for (int i = 0; i < numSamples; ++i)
    {
        sumLR += left[i] * right[i];
        sumLL += left[i] * left[i];
        sumRR += right[i] * right[i];
    }

    const double denominator = std::sqrt(sumLL * sumRR);
    const double correlation = (denominator < 1e-16) ? 1.0 : sumLR / denominator;

    return juce::jlimit(-1.0f, 1.0f, static_cast<float>(correlation));
}