SpectrumAnalyzer

About sylwekkominek / SpectrumAnalyzer

SpectrumAnalyzer was created because, since childhood, I liked watching the “jumping bars” moving in sync with music on my Technics EC-EH550 stereo system. Unfortunately, modern audio equipment with built-in spectrum analyzers is no longer available. There are many visualizer apps online, but they usually have limitations: they offer only a few adjustable parameters, which quickly become boring, or they rely on microphone input — which is not ideal because it picks up ambient noise, room echo, and the microphone changes the frequency response. SpectrumAnalyzer in its basic version works well with a microphone, but it is also easy to run on a Raspberry Pi, where you can capture audio samples from an optical input. I use a HiFiBerry DAC + DSP board for this purpose. One of the strengths of the application is its flexibility — users can freely modify colors and frequency bands. The app may not look spectacular yet, but it has great potential for experimentation and educational purposes. It helps new users easily get familiar with digital signal processing (DSP) and graphics programming.


sylwekkominek / SpectrumAnalyzer architecture

SpectrumAnalyzer architecture

The most important class in the program is AudioSpectrumAnalyzer. It manages the whole process using several threads that run in parallel to efficiently process data. These threads are:


Overview of audioConfig.py

This file handles input configuration and data acquisition for the SpectrumAnalyzer program. While it is primarily designed for capturing audio input from hardware devices, it can be adapted for other types of input signals as well. The module initializes the input stream with parameters such as data length and sampling rate (usually provided from config.py), and provides a function to read and unpack raw data from the input device, typically stereo audio channels (left and right). Importantly, this file can be modified or replaced to simulate input data instead of reading from actual hardware. This makes it a valuable tool for testing, debugging, and experimenting with digital signal processing (DSP) algorithms without requiring real hardware input. Users can generate synthetic signals (e.g., sine waves, test patterns) or feed in data from alternative sources such as wave files or sensor devices like accelerometers. This flexibility allows developers and hobbyists to easily prototype and validate DSP techniques, explore different input scenarios, and ensure the program behaves as expected under various conditions — all without the need for a physical audio input device.


Audio Input Device

Initialization

Collecting data from hardware


Overview of config.py

The file config.py contains the core configuration functions for the SpectrumAnalyzer program.
It defines window sizes, sampling parameters, colors, and shader code for visualization.


Window Settings

Fullscreen Default State

Maximized Window Size

Normal Window Size


Sampling and Signal Parameters

Number of Samples

Sampling Rate

Desired Frame Rate

Number of Signals for Averaging:

Number of Signals for Max Hold:

Alpha Factor for Smoothing:

Signal Window:

Scaling factor:

Offset factor:


Visual Settings

Selecting frequencies / Number of bars:

Static lines color:

Horizontal Line Positions in dBFS:

Rectangle colors for vertices:

Small rectangle colors for vertices:

Dynamic Max Hold visibility state

Dynamic Max Hold rectangle height

Dynamic Max Hold speed of falling

Dynamic Max Hold falling acceleration


Fragment Shader Code (GLSL)


Playing with graphics

Example 1

Example 2

Example 3

Example 4

Example 5

#if you get errors after modification please find following log with error msg: "VS log:"
def getAdvancedColorSettings():
    return '''#version 330 core

        in vec4 calculatedPosition;
        in vec4 vertColor;
        out vec4 Color;

        void main()
        {
            Color = vertColor;

        }
        '''

# Each rectangle consists of 2 triangles

# 3_____ 2   2
# |     /   /|
# |    /   / |
# |   /   /  |
# |  /   /   |
# | /   /    |
# |/   /_____|
# 0   0      1


def getColorsOfRectangle(vertex):
    match vertex:
        case 0:
            return (0.61, 0.61, 0.61,1)
        case 1:
            return (0,0,0,1)
        case 2:
            return (0,0,0,1)
        case 3:
            return (0.61, 0.61, 0.61,1)    

Example 6


def getFrequencies():
    return (20,40,60,80,100,120,150,180,220,250,300,330,360,400,440, 480,520,560,600,720)


# Each rectangle consists of 2 triangles

# 3_____ 2   2
# |     /   /|
# |    /   / |
# |   /   /  |
# |  /   /   |
# | /   /    |
# |/   /_____|
# 0   0      1

def getColorsOfRectangle(vertex):
    match vertex:
        case 0:
            return (1,0,0,1) #red
        case 1:
            return (0,0,1,1) #blue
        case 2:
            return (0,0,1,1) #blue
        case 3:
            return (1,0,0,1) #red

Example 7


# Each rectangle consists of 2 triangles

# 3_____ 2   2
# |     /   /|
# |    /   / |
# |   /   /  |
# |  /   /   |
# | /   /    |
# |/   /_____|
# 0   0      1

def getColorsOfRectangle(vertex):
    match vertex:
        case 0:
            return (0, 0,1,1) # blue
        case 1:
            return (1,0,0,1)  # red
        case 2:
            return (1,0,0,1)  # red
        case 3:
            return (0,0,1,1)  # blue

Example 8


# Each rectangle consists of 2 triangles

# 3_____ 2   2
# |     /   /|
# |    /   / |
# |   /   /  |
# |  /   /   |
# | /   /    |
# |/   /_____|
# 0   0      1

def getColorsOfRectangle(vertex):
    match vertex:
        case 0:
            return (1,0,0,1) # red
        case 1:
            return (1,0,0,1) # red
        case 2:
            return (0,0,1,1) # blue
        case 3:
            return (0,0,1,1) # blue

Example 9


# Each rectangle consists of 2 triangles

# 3_____ 2   2
# |     /   /|
# |    /   / |
# |   /   /  |
# |  /   /   |
# | /   /    |
# |/   /_____|
# 0   0      1

def getColorsOfRectangle(vertex):
    match vertex:
        case 0:
            return (0,0,1,1) # blue
        case 1:
            return (0,0,1,1) # blue
        case 2:
            return (1,0,0,1) # red
        case 3:
            return (1,0,0,1) # red

Example 10


# Each rectangle consists of 2 triangles

# 3_____ 2   2
# |     /   /|
# |    /   / |
# |   /   /  |
# |  /   /   |
# | /   /    |
# |/   /_____|
# 0   0      1

def getColorsOfRectangle(vertex):
    match vertex:
        case 0:
            return (0, 0,1,1) # blue
        case 1:
            return (0,0,0.2,1) # dark blue
        case 2:
            return (0,0,0.2,1) # dark blue
        case 3:
            return (0,0,1,1)  #  blue

Example 11

#if you get errors after modification please find following log with error msg: "VS log:"
def getAdvancedColorSettings():
    return '''#version 330 core

        in vec4 calculatedPosition;
        in vec4 vertColor;
        out vec4 Color;

        void main()
        {
            vec4 tmpColor;

            float t = clamp(calculatedPosition.y, -1.0, 1.0);

            //Move position from [-1,1] to [0,1]
            float y = (t + 1.0) * 0.5;

            vec4 red    = vec4(1.0, 0.0, 0.0,1);
            vec4 yellow = vec4(1.0, 1.0, 0.0,1);
            vec4 green  = vec4(0.0, 1.0, 0.0,1);
            vec4 cyan   = vec4(0.0, 1.0, 1.0,1);
            vec4 blue   = vec4(0.0, 0.0, 1.0,1);

            //Use 4 segments: [0,0.25), [0.25,0.5), [0.5,0.75), [0.75,1.0]

            tmpColor = mix(blue, cyan, smoothstep(0.0, 0.25, y));
            tmpColor = mix(tmpColor ,green, smoothstep(0.25,0.5, y));
            tmpColor = mix(tmpColor ,yellow, smoothstep(0.5,0.75, y));
            tmpColor = mix(tmpColor ,red, smoothstep(0.75,1, y));

            

            // removing:  Color = mix(tmpColor, vertColor, 0.4);
            // adding:
            Color = tmpColor;
        }
        '''

Example 12


def getAdvancedColorSettings():
    return '''#version 330 core

        in vec4 calculatedPosition;
        in vec4 vertColor;
        out vec4 Color;

        void main()
        {
            vec4 tmpColor;

            float t = clamp(calculatedPosition.x, -1.0, 1.0); //changing y to x

            //Move position from [-1,1] to [0,1]
            float y = (t + 1.0) * 0.5;

            vec4 red    = vec4(1.0, 0.0, 0.0,1);
            vec4 yellow = vec4(1.0, 1.0, 0.0,1);
            vec4 green  = vec4(0.0, 1.0, 0.0,1);
            vec4 cyan   = vec4(0.0, 1.0, 1.0,1);
            vec4 blue   = vec4(0.0, 0.0, 1.0,1);

            //Use 4 segments: [0,0.25), [0.25,0.5), [0.5,0.75), [0.75,1.0]

            tmpColor = mix(blue, cyan, smoothstep(0.0, 0.25, y));
            tmpColor = mix(tmpColor ,green, smoothstep(0.25,0.5, y));
            tmpColor = mix(tmpColor ,yellow, smoothstep(0.5,0.75, y));
            tmpColor = mix(tmpColor ,red, smoothstep(0.75,1, y));

            Color = tmpColor;

        }
        '''


This is where the real fun starts

You can simply copy and paste the contents of the getAdvancedColorSettings() function into ChatGPT and ask it to do something fancy. You don’t even have to understand the function — just ask and experiment with it. The animation changes over time, so you can see the effect evolve as it runs. Here’s what I got:

Example 13

Example 14

Example 15

Example 16

Example 17