top of page

Using Processing for Music Visualization

Writer's picture: Kassian HoubenKassian Houben

In this tutorial we will be covering the usage of Processing for music visualization. Adding an animated component to generative artwork often relies on the use of some form of random function, whether it be Perlin noise, the random() function, or another self devised method. Using music to provide this input data can add a very visually pleasing result as it brings together two senses in a synergetic way. The highs and lows of the music are reflected in the highs and lows of the visuals.


This leads us to the question: How do we use music as our input?


Let us first define our goal and take a look at the tools needed for this project.



Goal



Our goal is to create this visualizer. As you can see, some of the lines extending from the circle in the center are changing length based on musical input. You can also see that there is other non-connected movement occurring to add more interest. The program works in real-time and we will be able to use our own song file to replicate these results.


A link to the code repository is given at the end.



Tools


Following are the tools used to create this program:


Processing - A graphical library based on the Java programming language. Some familiarity will be useful for following this tutorial.


Minim - An easily imported library that allows for the analysis of sound. It can also do more but we will leave that for another tutorial. To import click Sketch -> Import Library -> Add Library.. and search for Minim.


Lastly, we need some music. An mp3 file of a song you like should work, just make sure to put it into a folder named data in your Processing sketch folder.



Understanding our input


We can now return to the main question:


How do we use music as our input?


Music is perceived via sound waves. A speaker produces a vibration in the air at a certain frequency which hits our eardrum and is translated by our brain into a sound. The frequency of this vibration determines whether we perceive it as a low or a high sound. In real life, many different frequencies hit our eardrum in quick succession, which means that ultimately a sound can be broken down into a collection of frequencies and their loudness/amplitude.


Digitally, this can be represented by splitting up our song at a given moment into many frequency “bands”. Each band contains the amplitude information for that specific frequency.



In this video, the brighter the orange, the louder the frequency. Lower frequencies are at the bottom, higher at the top. As you can see, elements like the kick drum immediately stand out as bright, thick, orange, vertical lines. You can also see the bass as a bright, thick, horizontal line below the kick drum. This demonstrates the basic way we can deconstruct our song and use it as an input.


In Processing, there are two main ways we can retrieve data from a slice of music: amplitude, and FFT (Fast Fourier Transform). The first method gives us a number representing the overall loudness of the song at a given point. The second method uses the loudness information from the individual frequency bands mentioned before. We will be using FFT for our purpose since it allows closer control and more variety for our animation.



Steps


1. Imports and Global Variables


To start we will need to import our Minim library, make sure you have it installed via the editor.

import ddf.minim.*;
import ddf.minim.analysis.*;
import ddf.minim.effects.*;
import ddf.minim.signals.*;
import ddf.minim.spi.*;
import ddf.minim.ugens.*;

We then specify some global configuration variables which define some of the most important elements we want control over. The smoothingFactor is a way to control how quickly our visuals respond to audio. Too fast and the animation will seem jittery, too slow and it will lag behind. The rest are self explanatory but remember to put an audio file in your data folder and change the name down below to your own mp3 file name.

// Configuration variables
// ------------------------
int canvasWidth = 1080;
int canvasHeight = 1080;

String audioFileName = "kingfisher.mp3"; // Audio file in data folder

float fps = 30;
float smoothingFactor = 0.25; // FFT audio analysis smoothing factor
// ------------------------

More global variables follow with some necessary definitions and variables that will be used later on.

// Global variables
AudioPlayer track;
FFT fft;
Minim minim;  

// General
int bands = 256; // must be multiple of two
float[] spectrum = new float[bands];
float[] sum = new float[bands];


// Graphics
float unit;
int groundLineY;
PVector center;

Lastly we will define some settings to set the size of the canvas and set our anti-aliasing to the highest smoothing level to ensure our lines are drawn as smoothly as possible. If your program spits out any related errors, try to reduce this level to 4, or even 3 (which is the default).

void settings() {
  size(canvasWidth, canvasHeight);
  smooth(8);
}


2. setup()


The setup() function consists of the initialization of some basic variables.


First we set the framerate and set a number of variables that will affect the positioning and sizing of our drawings. The unit variable is especially useful for creating scalable visuals as every drawn element references it including the setting of line thickness, circle size, and the size of each element relative to the canvas.

void setup() {
  frameRate(fps);

  // Graphics related variable setting
  unit = height / 100; // Everything else can be based around unit to make it change depending on size 
  strokeWeight(unit / 10.24);
  groundLineY = height * 3/4;
  center = new PVector(width / 2, height * 3/4);  

  minim = new Minim(this);
  track = minim.loadFile(audioFileName, 2048);
 
  track.loop();
 
  fft = new FFT( track.bufferSize(), track.sampleRate() );
 
  fft.linAverages(bands);
 
  // track.cue(60000); // Cue in milliseconds
}

The Minim class is initialized to allow for track loading and playing using track.loop(). We also initialize the fft variable using the track’s buffer size and sample rate. In essence, the buffer size is the amount of time you give the computer to process a piece of audio. Lower buffer size will reduce latency which is especially important if live input is critical. The sample rate describes how many audio samples the program can capture in a second. This is important for capturing the full range of frequencies that humans can hear. I won’t go into more detail here, but if you’d like to know more you can visit this link.


linAverages specifies how many frequency bands we would like to retrieve. For our program this is 256 which will be enough for our purposes since we are not planning to make an analysis tool.


Lastly, if you would like to cue your song to start somewhere other than the beginning, you can uncomment the last line and set the position in milliseconds.



3. drawStatic() and drawAll()


Before we move onto the draw() function which you might be expecting, we are going to have a look at two functions that handle all the actual work, drawStatic() and drawAll(). From now on, make sure to preserve the indentation found in these code snippets as I will be commenting on code in the middle of functions. For organisations sake, you might also want to put these two functions and their associated global variables in a new tab named draw_all.


drawStatic()



The drawStatic() function is responsible for drawing a number of extending lines that are not animated.


First we will need to define some more global variables. These variables are used to handle logic between the two functions and store details for faster re-drawing. extendingSphereLinesRadius sets the initial radius of each line.

int sphereRadius;

float spherePrevX;
float spherePrevY;

int yOffset;

boolean initialStatic = true;
float[] extendingSphereLinesRadius;

We then start the drawStatic() function, iterating through 240 angles in steps of 4 to draw the extending lines.

void drawStatic() {
 
  if (initialStatic) {
    extendingSphereLinesRadius = new float[241];
 
    for (int angle = 0; angle <= 240; angle += 4) {
      extendingSphereLinesRadius[angle] = map(random(1), 0, 1, sphereRadius, sphereRadius * 7);
    }
 
    initialStatic = false;
  }

A point is created using basic trigonometry to calculate the position based on the angle.

  // More extending lines
  for (int angle = 0; angle <= 240; angle += 4) {

    float x = round(cos(radians(angle + 150)) * sphereRadius + center.x);
    float y = round(sin(radians(angle + 150)) * sphereRadius + groundLineY - yOffset);
 
    float xDestination = x;
    float yDestination = y;

In this for loop, we continue to incrementally increase the length of the line while checking if our line is overlapping with the ground line. This is important since the ground line is a sine wave and constantly moving so we cannot simply check whether our line is more than a given Y value. The loop will continue until our previously initialized length has been reached or the ground has been hit. It almost acts like a ray-tracing algorithm.

    // Draw lines in small increments to make it easier to work with 
    for (int i = sphereRadius; i <= extendingSphereLinesRadius[angle]; i++) {
      float x2 = cos(radians(angle + 150)) * i + center.x;
      float y2 = sin(radians(angle + 150)) * i + groundLineY - yOffset;
 
      if (y2 <= getGroundY(x2)) { // Make sure it doesn't go into ground
        xDestination = x2;
        yDestination = y2;
      }
    }

Knowing our beginning and ending position, we draw a line between the two positions.

    stroke(255);
 
    if (y <= getGroundY(x)) {
      line(x, y, xDestination, yDestination);
    }
  }
}


drawAll()


The drawAll() function is responsible for drawing everything including a call to drawStatic(), the circles surrounding the sphere, the visualizer lines, and the ground sine wave.


Here we set some initial variables. Note the sum array passed in. It contains the loudness/amplitude values of each frequency band analyzed at the current time. It is provided by the draw() function which we’ll look at later.

void drawAll(float[] sum) {
  // Center sphere
  sphereRadius = 15 * round(unit);

  spherePrevX = 0;
  spherePrevY = 0;

  yOffset = round(sin(radians(150)) * sphereRadius);

  drawStatic();
 
  // Lines surrounding
  float x = 0;
  float y = 0;
  int surrCount = 1;

Here we draw the circles that are creating movement around the sphere. They only rely on the current frame count for movement.

  boolean direction = false;
 
  while (x < width * 1.5 && x > 0 - width / 2) {

    float surroundingRadius;
 
    float surrRadMin = sphereRadius + sphereRadius * 1/2 * surrCount;
    float surrRadMax = surrRadMin + surrRadMin * 1/8;

    float surrYOffset;
 
    float addon = frameCount * 1.5;
 
    if (direction) {
      addon = addon * 1.5;
    }

    for (float angle = 0; angle <= 240; angle += 1.5) {
 
      surroundingRadius = map(sin(radians(angle * 7 + addon)), -1, 1, surrRadMin, surrRadMax); // Faster rotation through angles, radius oscillates
 
      surrYOffset = sin(radians(150)) * surroundingRadius;

      x = round(cos(radians(angle + 150)) * surroundingRadius + center.x);
      y = round(sin(radians(angle + 150)) * surroundingRadius + getGroundY(x) - surrYOffset);

      noStroke();
      fill(map(surroundingRadius, surrRadMin, surrRadMax, 100, 255));
      circle(x, y, 3 * unit / 10.24);
      noFill();
    }

    direction = !direction;
 
    surrCount += 1;
  }


The sequence of if else statements below make up the most important section of the code. Part of what makes this visualizer unique is that frequencies are not displayed around the circle from low to high, left to right. They are instead displayed with lower frequencies in the center top of the circle and higher frequencies on the left and right ends.


These if else statements determine which frequency bands to use at which angle. Values are mapped approximately between the radius minus 1/8th of itself and 1.5 times a previously defined maximum. Lows, mids, and highs, are all mapped slightly differently to adjust for volume differences of the different bands. This was done manually with a lot of trial and error.

  // Lines extending from sphere
  float extendingLinesMin = sphereRadius * 1.3;
  float extendingLinesMax = sphereRadius * 3.5; 
 
  float xDestination;
  float yDestination;
 
  for (int angle = 0; angle <= 240; angle++) {

    float extendingSphereLinesRadius = map(noise(angle * 0.3), 0, 1, extendingLinesMin, extendingLinesMax);
 
    // Radius are mapped differently for highs, mids, and lows - alter higher mapping number for different result (eg. 0.8 to 0.2 in the highs)
    if (sum[0] != 0) {
      if (angle >= 0 && angle <= 30) {
        extendingSphereLinesRadius = map(sum[240 - round(map((angle), 0, 30, 0, 80))], 0, 0.8, extendingSphereLinesRadius - extendingSphereLinesRadius / 8, extendingLinesMax * 1.5); // Highs
      }
 
      else if (angle > 30 && angle <= 90) {
        extendingSphereLinesRadius = map(sum[160 - round(map((angle - 30), 0, 60, 0, 80))], 0, 3, extendingSphereLinesRadius - extendingSphereLinesRadius / 8, extendingLinesMax * 1.5); // Mids
      }
 
      else if (angle > 90 && angle <= 120) {
        extendingSphereLinesRadius = map(sum[80 - round(map((angle - 90), 0, 30, 65, 80))], 0, 40, extendingSphereLinesRadius - extendingSphereLinesRadius / 8, extendingLinesMax * 1.5); // Bass
      }
 
      else if (angle > 120 && angle <= 150) {
        extendingSphereLinesRadius = map(sum[0 + round(map((angle - 120), 0, 30, 0, 15))], 0, 40, extendingSphereLinesRadius - extendingSphereLinesRadius / 8, extendingLinesMax * 1.5); // Bass
      }
 
      else if (angle > 150 && angle <= 210) {
        extendingSphereLinesRadius = map(sum[80 + round(map((angle - 150), 0, 60, 0, 80))], 0, 3, extendingSphereLinesRadius - extendingSphereLinesRadius / 8, extendingLinesMax * 1.5); // Mids
      }
 
      else if (angle > 210) {
        extendingSphereLinesRadius = map(sum[160 + round(map((angle - 210), 0, 30, 0, 80))], 0, 0.8, extendingSphereLinesRadius - extendingSphereLinesRadius / 8, extendingLinesMax * 1.5); // Highs
      }
    }


Here is another implementation of the incremental ray tracer, making sure that lines don’t go past the sine wave ground line.

    x = round(cos(radians(angle + 150)) * sphereRadius + center.x);
    y = round(sin(radians(angle + 150)) * sphereRadius + groundLineY - yOffset);

    xDestination = x;
    yDestination = y;

    for (int i = sphereRadius; i <= extendingSphereLinesRadius; i++) {
      int x2 = round(cos(radians(angle + 150)) * i + center.x);
      int y2 = round(sin(radians(angle + 150)) * i + groundLineY - yOffset);
 
      if (y2 <= getGroundY(x2)) { // Make sure it doesnt go into ground
        xDestination = x2;
        yDestination = y2;
      }
    }


Lastly, we draw the ground line using the getGroundY() function (discussed in the next step). I hope I have not lost you in this process. Many of these calculations have taken hours of experimentation, they did not just magically come to mind. At its root, everything consists of simple trigonometry with some adjustments to make it all look nice.

    stroke(map(extendingSphereLinesRadius, extendingLinesMin, extendingLinesMax, 200, 255));
 
    if (y <= getGroundY(x))  {
      line(x, y, xDestination, yDestination);
    }
  }

  // Ground line
  for (int groundX = 0; groundX <= width; groundX++) {

    float groundY = getGroundY(groundX);

    noStroke();
    fill(255);
    circle(groundX, groundY, 1.8 * unit / 10.24);
    noFill();
  }
}


4. getGroundY()


Before we jump into the draw() function and our last step, we need to take a look at one last function: getGroundY(). It is referenced in many other places including the code just above.


getGroundY() is responsible for returning the Y position of the ground given an X position. Since the ground is a sine wave and animated, this function is used for redrawing and to ensure any extending lines do not intercept it. As you can see, the unit variable is used to ensure the ground has the same number of “waves” no matter the resolution.

// Get the Y position at position X of ground sine wave
float getGroundY(float groundX) {

  float angle = 1.1 * groundX / unit * 10.24;

  float groundY = sin(radians(angle + frameCount * 2)) * unit * 1.25 + groundLineY - unit * 1.25;

  return groundY;
}


5. draw()


Finally we reach the draw() function. It is short and sweet and its only responsibility is to keep the track playing, analyzing the current position, using our smoothingFactor to create the sum array, and calling our drawAll() function.

void draw() {
  fft.forward(track.mix);
 
  spectrum = new float[bands];
 
  for(int i = 0; i < fft.avgSize(); i++)
  {
    spectrum[i] = fft.getAvg(i) / 2;
 
    // Smooth the FFT spectrum data by smoothing factor
    sum[i] += (abs(spectrum[i]) - sum[i]) * smoothingFactor;
  }
 
  // Reset canvas
  fill(0);
  noStroke();
  rect(0, 0, width, height);
  noFill();
 
  drawAll(sum);
}


Conclusion


While there are many simpler ways to create a visualizer, I have found that these are usually much less interesting. I hope this tutorial has been both informative and interesting and that you feel ready to dive deeper into the creation of your own visualizer or to modify this one. In either case, I would love to see what you make using this tutorial so feel free to contact me on my website down below.


For the full code, with even more functionality, visit my Github repository. You can also view a fully visualized song here.



About the author


Kassian Houben (Estlin) is a musician and creative tech enthusiast from New Zealand. He made this project for his 2021 release “Imperative” to add an animated element to his cover art. You can find the EP on his website and on all music streaming/buying platforms.


Cover art for the EP:



32,877 views7 comments

Recent Posts

See All

7 Comments


Nandita Suri
Nandita Suri
Dec 18, 2024

Experience the fast-paced fun of Andar Bahar at Khelraja! This classic Indian card game is all about quick decisions and big rewards. Our user-friendly platform offers smooth gameplay, real-time action, and live dealers for an authentic Andar Bahar Game experience. Join now and test your luck with every card dealt!


Like

pigag56628
Oct 17, 2024

Using Processing for music visualization is such a fascinating way to blend art and technology! The ability to create dynamic visuals that respond to sound adds an incredible layer to the musical experience. For anyone looking to take their projects further, incorporating audio visual solutions can enhance both the visual and auditory elements, creating an immersive environment that captivates the audience. It's exciting to see how these tools can bring creativity to life!

Like

Tom Taylor
Tom Taylor
Oct 16, 2024

Você sabe de algum lugar onde possa experimentar os mesmos sentimentos que experimentaria em um cassino de apostas? porque ganhar é uma experiência maravilhosa, não importa se for em um cassino virtual. É evidente que a moderação no conhecimento é essencial, assim como em qualquer outro tema. A gama de jogos no bet da sorte corresponde às minhas preferências e eu jogo nele frequentemente.

Like

Lewis Wood
Lewis Wood
Sep 19, 2024

Music visualization can be a fascinating way to merge the auditory and visual senses, creating an immersive experience. When using Processing for this purpose, the synergy between sound and visuals can be incredibly captivating. Whether you're working with Perlin noise or a random function, music provides a dynamic source of input data that reflects beautifully in animated art. To take this to the next level, you can explore the process in greater detail through tutorials like this one, which demonstrates how the highs and lows of sound correspond directly to changes in visual patterns. For more in-depth insights on techniques and project guidance, consider visiting 2d animation agency where you’ll not only find resources but also get a sense of…

Like

Mark Millin
Mark Millin
May 28, 2024

Using Processing for music visualization is a fascinating way to combine coding with creative expression. Processing provides a flexible platform to create real-time visualizations that react to music, enhancing the overall experience. For those looking to enhance their music presence on streaming platforms, consider using Spotify promotion services from https://promosoundgroup.net/collection/streaming-promotion/spotify-promotion/ They offer strategies to increase visibility and reach a broader audience, much like how music visualization enhances the auditory experience with visual elements.

Like
bottom of page