You can’t hear DC

Recently one of my team members found a bug in some old code while doing a code review.  Our application was generating a sine wave to be rendered by the audio hardware.  The sample format isn't important except to note that it is an unsigned value between 0 and MAX = 2*FS_AMP.  The bug is in the following code.

void GenerateSineWave(float buffer[], float frequency, float amplitude) {
    // frequency in Hz, amplitude in range(0.0, 1.0)
    float w = 2 * PI * frequency / SAMPLE_RATE;
    for (int t = 0; t < NUMBER_OF_SAMPLES; t++)
        float sample = sin(w * t) * FS_AMP // full scale sine wave
        sample += FS_AMP; // Move into positive range 
        sample *= amplitude;  // Scale to requested amplitude
        buffer[t] = sample;

So do you see the bug?  After this code, we have a sine wave that oscillates between 0 and 2*amplitude*FS_AMP.  For amplitude = 1.0, this is exactly what we want, but for any smaller amplitude, the wave is low-justified in the sample range.


What we really wanted here is a signal that is center-justified in the range, such as below.  We were sending the wrong samples!


The fix is fairly simple.  We want to scale (*= amplitude) before we translate (+= FS_AMP).  Then we get a sine wave that oscillates between (FS_AMP *(1-amplitude)) and (FS_AMP * (1+amplitude)).

After fixing a bug in old code, the next question we have to ask ourselves is why we missed it.  How come this bug wasn't found in a test pass when it was written, or any time in between?  The answer is almost as subtle as the code that caused the problem.  From an audio perspective, the bug had no impact at all.

The buffer generated by this buggy code was sent directly to the DAC on the hardware.  The signal does not clip, and it's not distorted.  All of the sine components are the same.  If you got an FFT on each of the two sine waves above, they would differ only in the very first bin, the one corresponding to the DC component of the signal.

But you can't hear DC.

DC is essentially the baseline for a signal.  The value from which the signal wave deviates in time.  The measured level of silence.  For a speaker, the DC level is the distance of the cone when unplugged.  For the atmosphere, DC is about 14psi (or 1.0 atm) at sea level.

For a digital signal stored as numbers, DC can be any arbitrary value.  In a computer which have a fixed minimum and maximum, we usually choose halfway between min and max because it offers the most dynamic range.  This also explains why most audio formats use signed numbers for samples rather than unsigned.  The center of the allowed range for a signed number is zero, and zero is a very easy number to work with.  In the code snippet above, the bug would not manifest if we were using signed numbers, because the translation step for signed numbers is a no-op.

One other way of looking at DC is as a wave with frequency zero, and an infinite wavelength.  Since zero is below the lowest frequency a human can hear (about 20Hz), you can't hear it.  Q.E.D.

Epilogue: This bug does show up as a frequency component any time we dynamically alter the amplitude while streaming, so we fixed it.

Comments (4)
  1. geraldtubing says:

    Also, won’t the fix help save a bit of energy, because the currents flowing through the speaker’s coil are lower? hides

  2. The constant component also does strange things to mxing, depending on how you mix.

  3. Chalain says:

    I have resigned myself to the fact that I will not live long enough to take a true DC measurement.

  4. Brandon P says:

    In the real world, when that sound comes out of the speaker, it will sound different. This is why amplifiers incorporate a 5 Hz high-pass filter to try to bring this DC offset to a minimum.

    The reason is that real-world speaker drivers are non-linear in their response. They have mechanisms that are constantly bringing that speaker cone back to 0.

Comments are closed.

Skip to main content