tech dumps

hello@llwyd.io

31 January 2026

pinkish

As a long time Tinnitus sufferer (twenty years!) I find pink noise useful as a way to mask it or ignore it for a while. It is also useful for helping me concentrate in noisy environments such as an open plan office. I’ve written about pink noise before and how you can use the Voss-McCartney algorithm to generate it in real time1. Another method is to generate white noise (which has a flat frequency response, i.e. equal energy across all frequencies) and to filter it so that it has the characteristic -10db per decade roll-off. There are some existing IIR filter designs1,2 out there that will produce the required frequency response. I had the idea of generating pink noise using a graphic equaliser so that the resulting noise can be tweaked to personal preference.

PINKish

PINKish is a real time noise generator with 6-band graphic equaliser (EQ) that allows the listener to fine-tune the noise to their preference. The generator also has basic Automatic Gain Control (AGC)3 so that the overall volume is consistent irrespective of the EQ configuration. I developed a model of the EQ in Python so that I could manually adjust the gains to produce a pink(ish) frequency response. GitHub containing the code and the model is available here.

pinkish

PINKish noise generator running on Mac OS. graphic design GUI layouts are my passion.4

The signal processing chain is summarised in the following diagram:

pinkish

Signal processing flowchart for PINKish

Crossover Filters

The equaliser was implemented as a series of digital ‘Linkwitz-Riley’ crossover filters5. They work as a pair of low pass and high pass filters that split the incoming signal into respective low / high frequency bands. Each band has a gain applied before being summed together again:

pinkish

Linkwitz-Riley Crossover filter principle5.

The output of the LR crossover filter can be cascaded to further isolate higher (or lower) frequency bands. Here the output of the HPF is the input to another LR crossover filter to further split the frequency content:

pinkish

Cascading Linkwitz-Riley Crossover filters.

After each crossover stage the output of the LPF goes through a gain stage and the output of the HPF becomes the input of another crossover filter. At the final crossover filter N both LPF and HPF outputs go through their respective gain stages. The output of all the gain stages are then summed to recombine all of the frequency bands to produce the output at the full bandwidth.

Filter Design

The filters used in the crossover stages are a pair of first-order digital Butterworth filters6. The SciPy library in python provides a handy function for generating the coefficients:

from scipy import signal

order = 1
cutoff = 1000
fs = 48000
lpf = signal.butter(order, cutoff, 'lowpass', fs=fs, output='sos')
hpf = signal.butter(order, cutoff, 'highpass', fs=fs, output='sos')

The output representation of the filter is set to sos, which stands for Second-Order Sections (Also known as Biquads)7. This means that the filter can be implemented efficiently as a series of second order IIR filters using the Transposed Direct Form 2 topology8,9. The impulse response of the filter can be generated by filtering a Dirac function10 as follows:

num_samples = 1024
h = signal.unit_impulse(num_samples)

lpf_out = signal.sosfilt(lpf, h)
hpf_out = signal.sosfilt(hpf, h)

This can then be analysed and graphed using an FFT11 to view the spectral properties of the filters.

Equaliser Model

I developed an interactive model of the cascaded crossover filters using Python:

pinkish

PINKish filter design tool

This model allowed me to tweak the gain sliders for each band so that the resulting frequency response in the human hearing range12 (HR) was roughly pink-ish. The plot contains an ideal 1/f frequency response as a reference as well as a real-time measurement of the gradient. The Export button auto-generates some rust code containing the filter coefficients as well as the gains for the selected sampling frequency. Python code for the model can be found here, though it is admittedly very spaghetti.

Automatic gain control (AGC)

In order to appropriately amplify or attenuate the filtered audio some measure of signal power is necessary. I run the absolute value of the filtered audio through a single pole low pass filter with a low cutoff rate (1Hz) to get a rough measure of the average power for this period13. This measure can then be compared against a desired signal power and the amplification/attenuation applied appropriately.

For PINKish a delta is calculated between the measured and desired signal power. The sign of this delta determines whether the applied gain increases or decreases:

pub struct AGC{
    set_point:f32,
    gain:f32,
}

impl AGC{
const DELTA_SCALE:f32 = 0.0005;
    pub fn new(set_point:f32) -> AGC
    {
        AGC
        {
            set_point, // Desired signal power
            gain: 0.1, // Initial Gain
        }
    }
    pub fn update(&mut self, rms:f32)
    {
        let delta = self.set_point - rms;
        self.gain *= 1.0 + (delta * Self::DELTA_SCALE);
        
        assert!(self.gain > 0.0);
    }
}

The DELTA_SCALE constant is necessary so that the gain doesn’t increase or decrease too quickly. If the changes in gain are too large a noticeable “pumping” sound is present which is unpleasant to listen to. After the gain is updated it is applied to the filtered signal at the next sample.

Putting it together and testing

I implemented the app in rust using cpal14 as the audio playback engine and egui15 for the user interface. The full source is available here and the audio processing loop can be found here. Upon running the program you can hear filtered noise that can be adjusted in real time using the gain sliders.

I added some command line functionality so that I could verify the signal processing was correct. Upon passing the --pink or --white flag the program will generate audio data to the console instead of the sound-card. This meant I could pipe the output to a file and analyse using python:

$ cargo run -- -w 1000 > white.txt
$ cargo run -- -p 1000 > pink.txt

pinkish

1000x 1 second samples @ 48kHz with the EQ set to ‘Pink’

pinkish

1000x 1 second samples @ 48kHz with the EQ set to ‘White’

Conclusion

While the output isn’t perfect pink noise it is nonetheless soothing to listen to in the background when I’m trying to concentrate in a noisy environment. There’s still plenty of things to improve on and I intend to keep maintaining it but it works nice as a tunable noise generator intended for human ears.

References

  1. DSP generation of Pink (1/f) Noise link  2

  2. SPECTRAL AUDIO SIGNAL PROCESSING - Julius O. Smith III link 

  3. Wikipedia - Automatic Gain Control link 

  4. Know Your Meme - Graphic Design Is My Passion link 

  5. Linkwitz-Riley Crossovers: A Primer link  2

  6. Wikipedia - Butterworth Filter link 

  7. Introduction to Digital Filters - BiQuad Section - Julius O. Smith III link 

  8. Introduction to Digital Filters - Transposed Direct Forms - Julius O. Smith III link 

  9. This is what SciPy is doing under the hood when using sosfilt(), see here 

  10. Wikipedia - Dirac delta function link 

  11. numpy.fft.fft link 

  12. Wikipedia - Hearing Range link 

  13. Digital Envelope Detection: The Good, the Bad, and the Ugly link 

  14. docs.rs - cpal link 

  15. docs.rs - egui link