Yey

This sketch creates a mesmerizing 3D audio visualizer where a central rotating torus and 16 surrounding boxes react dynamically to microphone input. The shapes pulse, rotate, and change color in real-time based on different frequency bands of the audio spectrum, creating a dance-like visual representation of sound.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the boxes huge — The shapeSize range (20-150 pixels) controls the box dimensions—increasing the maximum makes them pulse much larger when the music hits
  2. Slow down the torus rotation — The millis() multipliers control how fast the central shape spins—smaller values make it rotate in slow motion
  3. Create a full-circle rainbow instead of 16 boxes — Increasing numShapes from 16 to 64 creates way more boxes, filling the orbit into an almost continuous circle of color
  4. Add a glowing background trail — Changing background(0) to a low value like 5 keeps previous frames visible, creating a motion blur effect as shapes dance
  5. Make treble frequencies ultra-shiny — Increasing the shininess range (1-50) makes the shapes reflect light dramatically more when high frequencies play
  6. Make all boxes the same color — Instead of mapping each box to its own frequency band for hue, use a shared color across all 16 boxes
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a fully 3D audio visualizer using WebGL, the p5.sound library, and FFT (Fast Fourier Transform) analysis. A central torus and 16 surrounding boxes respond to live microphone input, changing their size, rotation, color, and brightness based on different frequency bands in the audio spectrum. It's a beautiful demonstration of how p5.js can connect sound and visuals, and it teaches you to think about HSB color spaces, 3D transformations, and real-time data mapping.

The code is organized into setup() which initializes the WebGL canvas, audio input, and FFT analyzer; draw() which runs 60 times per second to animate the shapes based on current frequency energy; and helper functions for touch events and window resizing. By reading it, you'll learn how to feed microphone data into FFT, extract energy values from different frequency bands, and use map() to translate those numbers into visual properties like size, rotation, hue, and brightness.

⚙️ How It Works

  1. When the sketch first loads, setup() creates a full-window WebGL canvas, switches to HSB color mode for intuitive hue manipulation, and initializes the microphone and FFT analyzer. The sketch waits until the user touches the screen (or clicks) to activate audio input.
  2. Every frame, draw() clears the background to black and adds three 3D point lights to illuminate the shapes with depth and shadow.
  3. Once audio has started, FFT analyzes the live microphone input and extracts energy levels for six frequency bands: bass, mid, treble, overall, lowMid, and highMid.
  4. A central torus is drawn at the center, rotating continuously on X and Y axes. Its size, thickness, hue, saturation, brightness, and shininess all map to different energy bands—so bass energy changes its color, mid energy controls thickness, and treble controls how shiny it looks.
  5. Sixteen boxes are positioned in a circle around the center. Each box reacts to its own slice of the frequency spectrum (box 0 reacts to 20 Hz, box 1 to 40 Hz, etc.), creating a rainbow effect where different frequency bands make different shapes pulse and rotate.
  6. On mobile, the sketch displays 'Touch screen to start audio input' until the user touches, which activates the audio context—a requirement for Web Audio on most mobile browsers.

🎓 Concepts You'll Learn

WebGL 3D graphicsFFT audio analysisReal-time frequency bandsHSB color mode3D transformations (translate, rotate)Data mapping with map()Specular lighting and materialsMobile audio activationTouch event handling

📝 Code Breakdown

preload()

preload() runs before setup() and is the place to load images, fonts, and sounds. p5.js waits for all preload assets to finish loading before calling setup().

function preload() {
  painting_1772137068539 = loadImage('painting-1772137068539.png');
}
Line-by-line explanation (1 lines)
painting_1772137068539 = loadImage('painting-1772137068539.png');
Loads an image file from the sketch's folder (though this particular image is not used in the current draw loop—it's available for future use)

setup()

setup() runs once when the sketch starts. It's where you initialize the canvas, set up audio input, and configure drawing modes. Everything you set here in setup() affects the entire draw loop that follows.

function setup() {
  // Create a WebGL canvas that fills the window for 3D graphics
  createCanvas(windowWidth, windowHeight, WEBGL);
  
  // Set color mode to HSB for more intuitive color manipulation
  // Hue: 0-360 degrees (0/360=red, 120=green, 240=blue)
  // Saturation: 0-100% (0=grayscale, 100=vibrant)
  // Brightness: 0-100% (0=black, 100=white)
  colorMode(HSB, 360, 100, 100);
  
  // Initialize audio input (microphone)
  mic = new p5.AudioIn();
  mic.start(); // Start the microphone input
  
  // Initialize FFT (Fast Fourier Transform) for audio analysis
  fft = new p5.FFT();
  fft.setInput(mic); // Set the microphone as the input source for FFT

  // Disable stroke for the 3D shapes to give them a smoother look
  noStroke();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation WebGL Canvas Setup createCanvas(windowWidth, windowHeight, WEBGL);

Creates a 3D-capable canvas using WebGL that fills the entire window

configuration HSB Color Mode colorMode(HSB, 360, 100, 100);

Switches from RGB to HSB (Hue-Saturation-Brightness) for easier color manipulation based on audio frequency

configuration Microphone and FFT Initialization mic = new p5.AudioIn(); micfft = new p5.FFT(); fft.setInput(mic);

Sets up real-time microphone input and FFT spectrum analysis

createCanvas(windowWidth, windowHeight, WEBGL);
Creates a canvas as wide and tall as the browser window, and enables WEBGL mode for 3D rendering. WEBGL is p5.js's interface to hardware-accelerated 3D graphics.
colorMode(HSB, 360, 100, 100);
Switches to HSB (Hue-Saturation-Brightness) color space where hue is 0-360°, saturation is 0-100%, and brightness is 0-100%. This is perfect for mapping audio frequencies to a rainbow spectrum.
mic = new p5.AudioIn();
Creates a new AudioIn object that will capture microphone input from the user's device
mic.start();
Activates microphone input. On mobile, the user must touch the screen first (see touchStarted()) to allow browser access.
fft = new p5.FFT();
Creates a Fast Fourier Transform analyzer that will break the audio signal into frequency bands so you can detect bass, mid, treble, and other frequency ranges
fft.setInput(mic);
Tells the FFT to analyze the microphone signal instead of playing audio—now each frame you can call fft.getEnergy() to extract frequency data
noStroke();
Disables the outline stroke for all shapes so the torus and boxes appear smoother and more continuous

draw()

draw() runs 60 times per second. This is where all animation happens: audio analysis, position updates, rotations, and rendering. The push()/pop() pairs save and restore the transformation matrix so each shape's rotations don't affect others.

function draw() {
  // Clear the background to black
  background(0);

  // Add lighting for a 3D effect in WebGL mode
  // Point light from the top-left
  pointLight(255, 255, 255, -windowWidth/4, -windowHeight/4, 200);
  // Point light from the bottom-right with a slight blue tint
  pointLight(200, 200, 255, windowWidth/4, windowHeight/4, 200);
  // Directional light from the front
  directionalLight(255, 255, 255, 0, 0, -1);

  // Analyze the audio spectrum only after audio has started
  if (audioStarted) {
    fft.analyze();

    // Get energy levels for different frequency bands
    let bassEnergy = fft.getEnergy("bass");      // Low frequencies (0-140 Hz)
    let midEnergy = fft.getEnergy("mid");        // Mid frequencies (140-6000 Hz)
    let trebleEnergy = fft.getEnergy("treble");  // High frequencies (6000-20000 Hz)
    let overallEnergy = fft.getEnergy("overall"); // Total energy across the spectrum
    let lowMidEnergy = fft.getEnergy("lowMid");  // Low-mid frequencies
    let highMidEnergy = fft.getEnergy("highMid"); // High-mid frequencies

    // --- Central Reactive Shape (Torus) ---
    push();
    // Rotate the central shape based on time for continuous movement
    rotateX(millis() * 0.0005);
    rotateY(millis() * 0.001);
    
    // Map overall energy to size for the torus
    let torusSize = map(overallEnergy, 0, 255, 100, 300); // Overall size reactive to sound
    let torusThickness = map(midEnergy, 0, 255, 20, 80); // Thickness reactive to mid frequencies
    
    // Set material properties with HSB colors reactive to frequency bands
    // Hue: Rotate through the spectrum based on bass and highMid energies
    let torusHue = map(bassEnergy + highMidEnergy, 0, 510, 0, 360);
    // Saturation: More vibrant when mid and treble energies are high
    let torusSaturation = map(midEnergy + trebleEnergy, 0, 510, 50, 100);
    // Brightness: Brighter when overall energy is high
    let torusBrightness = map(overallEnergy, 0, 255, 50, 100);
    
    specularMaterial(torusHue, torusSaturation, torusBrightness);
    shininess(map(trebleEnergy, 0, 255, 1, 50)); // Shine reactive to treble
    
    // Draw the torus
    torus(torusSize * 0.7, torusThickness, 24, 16);
    pop();

    // --- Peripheral Reactive Shapes (Boxes) ---
    let numShapes = 16; // Number of shapes arranged in a circle
    let radius = min(width, height) * 0.4; // Radius for the circle of shapes

    for (let i = 0; i < numShapes; i++) {
      push();
      let angle = map(i, 0, numShapes, 0, TWO_PI); // Calculate angle for each shape
      
      // Position the shape in a circle around the center
      translate(radius * cos(angle), radius * sin(angle), 0);
      
      // Rotate the shape
      rotateZ(angle);
      rotateX(bassEnergy * 0.01); // Bass energy makes them rotate on X-axis
      rotateY(midEnergy * 0.01);  // Mid energy makes them rotate on Y-axis
      
      // Get energy for a specific frequency band for this peripheral shape
      // We're using a frequency band based on 'i' to get different reactions
      // fft.getEnergy() can also take a frequency value directly (e.g., 20, 40, 60...)
      let freqBandEnergy = fft.getEnergy(i * 20 + 20); // Each shape reacts to a different part of the spectrum
      let shapeSize = map(freqBandEnergy, 0, 255, 20, 150); // Size reactive to its frequency band
      
      // Set material for peripheral shapes with HSB colors
      // Hue: Maps to the individual frequency band, creating a spectrum effect
      let shapeHue = map(freqBandEnergy, 0, 255, 0, 360);
      // Saturation: More saturated when overall energy is high
      let shapeSaturation = map(overallEnergy, 0, 255, 60, 100);
      // Brightness: Brighter when the specific band energy is high
      let shapeBrightness = map(freqBandEnergy, 0, 255, 50, 100);
      
      specularMaterial(shapeHue, shapeSaturation, shapeBrightness);
      shininess(map(lowMidEnergy, 0, 255, 1, 30)); // Shine reactive to lowMid
      
      // Draw a box for each peripheral shape
      box(shapeSize, shapeSize, shapeSize / 2);
      pop();
    }
  } else {
    // Display an instruction for mobile users until audio is activated
    push();
    noLights(); // Disable lights for the text
    fill(255);
    textSize(20);
    textAlign(CENTER, CENTER);
    text("Touch screen to start audio input", 0, 0, 0); // Display instruction
    pop();
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

configuration 3D Lighting pointLight(255, 255, 255, -windowWidth/4, -windowHeight/4, 200); pointLight(200, 200, 255, windowWidth/4, windowHeight/4, 200); directionalLight(255, 255, 255, 0, 0, -1);

Adds three light sources to the 3D scene so shapes have shadows, depth, and reflective properties

calculation FFT Spectrum Analysis fft.analyze(); let bassEnergy = fft.getEnergy("bass"); let midEnergy = fft.getEnergy("mid"); let trebleEnergy = fft.getEnergy("treble"); let overallEnergy = fft.getEnergy("overall"); let lowMidEnergy = fft.getEnergy("lowMid"); let highMidEnergy = fft.getEnergy("highMid");

Analyzes the microphone input and extracts energy values (0-255) for six different frequency bands

for-loop Central Torus Shape push(); rotateX(millis() * 0.0005); rotateY(millis() * 0.001); let torusSize = map(overallEnergy, 0, 255, 100, 300); let torusThickness = map(midEnergy, 0, 255, 20, 80); let torusHue = map(bassEnergy + highMidEnergy, 0, 510, 0, 360); let torusSaturation = map(midEnergy + trebleEnergy, 0, 510, 50, 100); let torusBrightness = map(overallEnergy, 0, 255, 50, 100); specularMaterial(torusHue, torusSaturation, torusBrightness); shininess(map(trebleEnergy, 0, 255, 1, 50)); torus(torusSize * 0.7, torusThickness, 24, 16); pop();

Draws a rotating 3D torus at the center that changes size, color, and shine based on audio energy

for-loop Peripheral Boxes Loop for (let i = 0; i < numShapes; i++) { push(); let angle = map(i, 0, numShapes, 0, TWO_PI); translate(radius * cos(angle), radius * sin(angle), 0); rotateZ(angle); rotateX(bassEnergy * 0.01); rotateY(midEnergy * 0.01); let freqBandEnergy = fft.getEnergy(i * 20 + 20); let shapeSize = map(freqBandEnergy, 0, 255, 20, 150); let shapeHue = map(freqBandEnergy, 0, 255, 0, 360); let shapeSaturation = map(overallEnergy, 0, 255, 60, 100); let shapeBrightness = map(freqBandEnergy, 0, 255, 50, 100); specularMaterial(shapeHue, shapeSaturation, shapeBrightness); shininess(map(lowMidEnergy, 0, 255, 1, 30)); box(shapeSize, shapeSize, shapeSize / 2); pop(); }

Draws 16 boxes in a circle, each reacting to a different frequency band to create a rainbow spectrum effect

conditional Audio Prompt } else { push(); noLights(); fill(255); textSize(20); textAlign(CENTER, CENTER); text("Touch screen to start audio input", 0, 0, 0); pop(); }

Displays a message telling the user to touch the screen to activate audio on mobile devices

background(0);
Clears the entire canvas to black (hue 0, saturation 0, brightness 0 in HSB) every frame, removing the previous frame's shapes
pointLight(255, 255, 255, -windowWidth/4, -windowHeight/4, 200);
Creates a bright white point light positioned in the top-left corner of the 3D space, creating shadows and highlights on the shapes
fft.analyze();
Analyzes the current microphone audio input and breaks it into frequency bands—must be called every frame before you call getEnergy()
let bassEnergy = fft.getEnergy("bass");
Extracts a number (0-255) representing the current energy in the bass frequencies (0-140 Hz)—loud bass drum hits produce high values
let torusSize = map(overallEnergy, 0, 255, 100, 300);
Uses map() to convert the overall energy (0-255) into a torus radius (100-300 pixels)—the torus pulses bigger when the music is louder
let torusHue = map(bassEnergy + highMidEnergy, 0, 510, 0, 360);
Adds bass and highMid energies together (0-510 total) and maps them to a hue angle (0-360°), making the torus cycle through the color spectrum
specularMaterial(torusHue, torusSaturation, torusBrightness);
Sets the shiny, reflective material properties of the torus using HSB values so it reflects the 3D lights beautifully
let angle = map(i, 0, numShapes, 0, TWO_PI);
For each box (i from 0 to 15), calculates an angle evenly spaced around a circle (0 to 2π radians), so all 16 boxes spread out like points on a clock
translate(radius * cos(angle), radius * sin(angle), 0);
Moves the drawing position outward from the center using sine and cosine to place each box at its calculated angle on a circular orbit
let freqBandEnergy = fft.getEnergy(i * 20 + 20);
Each box reacts to a different frequency band: box 0 watches 20 Hz, box 1 watches 40 Hz, box 2 watches 60 Hz, etc., spreading the spectrum across all boxes
box(shapeSize, shapeSize, shapeSize / 2);
Draws a cube where the width and height equal shapeSize (square face) and depth is half that, creating a flat rectangular box that pulses based on its frequency band

touchStarted()

touchStarted() is a p5.js event function that runs whenever the user touches the screen. It's essential for mobile Web Audio, which requires user permission before accessing the microphone or starting audio playback.

function touchStarted() {
  // Start the audio context. This is required on many mobile browsers for audio to work.
  userStartAudio();
  audioStarted = true; // Set flag to true once audio is started

  return false; // Prevent default mobile browser behavior (e.g., scrolling, zooming)
}
Line-by-line explanation (3 lines)
userStartAudio();
Calls a p5.js built-in function that activates the Web Audio context. On mobile, user interaction (touch/click) is required before any audio can play or be analyzed.
audioStarted = true;
Sets the global audioStarted flag to true, which tells draw() to start analyzing the FFT and rendering the reactive shapes instead of showing the instruction text
return false;
Prevents the browser's default touch behavior (like scrolling or zooming the page) so the canvas stays stable when the user touches it

windowResized()

windowResized() is called automatically by p5.js whenever the window size changes. It's good practice to include it in sketches that use full-window canvases like this one to keep everything responsive.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // In WEBGL mode, perspective might need to be re-applied after resize
  perspective();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Whenever the browser window resizes (user rotates phone, resizes desktop window, etc.), this updates the p5.js canvas to match the new window size
perspective();
Resets the 3D perspective settings after the canvas resizes—this ensures the 3D projection stays correct for the new canvas dimensions

📦 Key Variables

painting_1772137068539 image

Stores a preloaded image file that could be used in future visualizations or as a texture

let painting_1772137068539;
mic p5.AudioIn object

Captures live microphone input from the user's device so the FFT can analyze it

let mic;
fft p5.FFT object

Analyzes the microphone audio and breaks it into frequency bands (bass, mid, treble, etc.) that you can query with getEnergy()

let fft;
audioStarted boolean

Tracks whether the user has touched the screen to activate audio on mobile—controls whether draw() shows shapes or the instruction text

let audioStarted = false;
bassEnergy number (0-255)

Current energy level in the bass frequency band (0-140 Hz)—used to drive rotation and color changes in the torus and boxes

let bassEnergy = fft.getEnergy('bass');
midEnergy number (0-255)

Current energy level in the mid-range frequencies (140-6000 Hz)—controls torus thickness, saturation, and box rotation

let midEnergy = fft.getEnergy('mid');
trebleEnergy number (0-255)

Current energy level in the high treble frequencies (6000-20000 Hz)—controls the shininess and brightness of the torus

let trebleEnergy = fft.getEnergy('treble');
overallEnergy number (0-255)

Total energy across all frequencies combined—drives the torus size and overall brightness of all shapes

let overallEnergy = fft.getEnergy('overall');
numShapes number

How many boxes orbit the central torus (currently 16)—increasing this creates a denser visual effect

let numShapes = 16;
radius number

The distance from the center where the orbiting boxes are positioned—40% of the smaller window dimension

let radius = min(width, height) * 0.4;
angle number

The angle (in radians) for each box's position around the circle—spreads 16 boxes evenly from 0 to 2π

let angle = map(i, 0, numShapes, 0, TWO_PI);
freqBandEnergy number (0-255)

The energy in a specific frequency band assigned to each box—makes box i react to frequency i*20+20 Hz

let freqBandEnergy = fft.getEnergy(i * 20 + 20);

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG draw() - FFT analysis

If the user denies microphone permission, the sketch will try to analyze an empty FFT and get 0 energy values forever, never visualizing anything

💡 Add a check for mic.enabled or wrap the fft.analyze() in a try-catch block, and display a more helpful error message if microphone access is denied

PERFORMANCE draw() - peripheral boxes loop

With 16 boxes, each one calls map() 6-7 times per frame (60 fps × 16 boxes = lots of calculations). The shininess() and specularMaterial() calls are also relatively expensive in WebGL.

💡 Cache energy band calculations outside the loop since they don't change per box: calculate all needed energies once before the loop, then reuse them inside

STYLE All functions

Energy values from getEnergy() are raw 0-255 numbers; adding two energies (bassEnergy + highMidEnergy) can exceed 255 and map unexpectedly. The code adjusts for this in torusHue (maps 0-510 instead of 0-255) but this pattern is not consistent.

💡 Always normalize combined energy values by dividing by 2 before mapping, or create a helper function to combine energies safely: let combinedEnergy = (bassEnergy + highMidEnergy) / 2;

FEATURE setup()

The preloaded painting image is never used in the sketch, though it's loaded in preload()

💡 Either remove the unused image to save memory, or use it as a texture on the 3D shapes via texture(painting_1772137068539) inside the box() and torus() calls

FEATURE touchStarted() / mobile audio

Desktop users with microphones see the 'Touch screen to start audio input' message, which is confusing—they should be able to click instead

💡 Rename touchStarted() to a general interaction handler and duplicate it as mousePressed(), or use a single event listener that responds to both touch and click

🔄 Code Flow

Code flow showing preload, setup, draw, touchstarted, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> canvas-creation[canvas-creation] setup --> color-mode[color-mode] setup --> audio-init[audio-init] setup --> lighting-setup[lighting-setup] click canvas-creation href "#sub-canvas-creation" click color-mode href "#sub-color-mode" click audio-init href "#sub-audio-init" click lighting-setup href "#sub-lighting-setup" draw --> audio-analysis[audio-analysis] draw --> central-torus[central-torus] draw --> peripheral-boxes-loop[peripheral-boxes-loop] draw --> audio-not-started[audio-not-started] click audio-analysis href "#sub-audio-analysis" click central-torus href "#sub-central-torus" click peripheral-boxes-loop href "#sub-peripheral-boxes-loop" click audio-not-started href "#sub-audio-not-started" audio-not-started -->|if audio not started| draw audio-analysis -->|FFT analysis| central-torus audio-analysis -->|FFT analysis| peripheral-boxes-loop central-torus -->|for-loop| draw peripheral-boxes-loop -->|for-loop| draw touchstarted[touchstarted] --> audio-init click touchstarted href "#fn-touchstarted" windowresized[windowresized] --> setup click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual experience does the Yey sketch create?

The Yey sketch generates dynamic 3D visualizations that respond to live audio input, creating an immersive experience that synchronizes colorful shapes with dance music.

How can users interact with the Yey sketch?

Users can interact by providing live audio input through their microphone, which influences the visual elements based on the sound frequencies detected.

What creative coding techniques are showcased in the Yey sketch?

The sketch demonstrates the use of WebGL for 3D graphics, audio analysis with FFT for real-time sound visualization, and color manipulation using HSB color mode.

Preview

Yey - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Yey - Code flow showing preload, setup, draw, touchstarted, windowresized
Code Flow Diagram