mj

Pink Noise

20251019

I became interested in this after messing around with using xoshiro to generate white noise.

Pink noise has a 1/f falloff. It is described as a more natural sounding noise, similar to a waterfall.

DSP generation of Pink (1/f) Noise is the most thourough information I could find.

There are 2 main ways to generate pink noise.

  1. Applying a pinking filter to white noise
  2. Using the Voss-McCartney algorithm

I am focussing on the Voss-McCartney algorithm because it can use the same xoshiro code I already created for generating white noise. Also, the algorithm itself seems kind of neat and interesting.

Voss-McCartney Algorithm

Pink noise can be made by adding together the output of several noise generators. Each generator's output is updated at half the rate of the previous.

xxxxxxxxxxxxxxxxx
x x x x x x x x x
x   x   x   x   x
x       x       x
x               x
  

    
void PinkNoise(float* pfBuffer, uint32_t uNumSamples, uint32_t uNumOctaves)
{
    const uint32_t kuMaxKey = ((uint32_t)1 << uNumOctaves) - 1); // uNumOctaves bits set
    uint32_t uKey = 0;
    RandomGenerator noise;
    float fNoiseVals[uNumOctaves];

    for(uint32_t uOctave = 0; uOctave < uNumOctaves; uOctave++)
    {
        fNoiseVals[uOctave] = noise.Generate() / uNumOctaves;
    }

    for(uint32_t uSampleIdx = 0; uSampleIdx < uNumSamples; uSampleIdx++)
    {
        float fSum = 0.0f;
        uint32_t uPrevKey = uKey;

        uKey++;
        if(uKey > kuMaxKey)
        {
            uPrevKey = 0;
            uKey = 1;
        }

        const uint32_t kuDiff = uPrevKey ^ uKey; // Bits that have changed

        for(uint32_t uOctave = 0; uOctave < uNumOctaves; uOctave++)
        {
            if(kuDiff & (1 << uOctave))
            {
                noiseVals[i] = noise.Generate() / uNumOctaves;
            }

            fSum += noiseVals[uOctave];
        }

        pfBuffer[uSampleIdx] = fSum;
    }
}
    
  

It is worth taking the time to make sense of the bit manipulation, and how that is generating the update pattern for the noise. It helps to plot out the binary numbers. The numbers 0 to 8 in binary representation are

0 1 2 3 4 5 6 7 8
0 1 0 1 0 1 0 1 0
0 0 1 1 0 0 1 1 0
0 0 0 0 1 1 1 1 0
0 0 0 0 0 0 0 0 1

The XOR operation gives the update rates we are aiming for.

0 1 2 3 4 5 6 7 8
0 1 1 1 1 1 1 1 1
0 0 1 0 1 0 1 0 1
0 0 0 0 1 0 0 0 1
0 0 0 0 0 0 0 0 1

    
const uint32_t kuMaxKey = ((uint32_t)1 << uNumOctaves) - 1);
    
  
This line is setting the lower kuNumOctaves bits of kuMaxKey. The code is from Stack Overflow. kuMaxKey is used to loop the updates of the noise generators when the lowest octave band is updated.

There wrapping of uKey resets the key to 1, and updates the previous key to 0. This is to ensure that the update of the noise generators at each octave is correct. If key was reset to 0, some of the higher octaves would miss an update due to the 0 XOR 8.