Luna's

Luna's is an interactive audio-reactive visualization that draws a mesmerizing rainbow spiral wave synchronized to live microphone input. Users can toggle between a colorful music-responsive spiral and a glitchy 'hacked screen' effect, with bonus text-to-speech functionality triggered by keyboard commands.

🧪 Try This!

Experiment with the code by making these changes:

  1. Toggle to the hacked screen effect
  2. Make the spiral tighter or looser — Change angleIncrement to control how densely packed the spiral points are; lower values (0.01) create tight coils, higher values (0.15) make it sparse and spiky
  3. Speed up the spiral expansion — Increase radiusIncrement to make the spiral expand faster outward; the spiral will reach the canvas edges quicker
  4. Make the spiral respond more dramatically to sound — Change the max wave offset from 30 to 100; the spiral will wobble wildly with every sound, creating a more exaggerated music visualization
  5. Make the spiral more colorful and dim — Reduce the saturation to make colors pastel, and lower the minimum brightness so the inner spiral is darker
  6. Add more glitch pixels to the hacked effect — Change the 10% glitch probability (0.1) to 30% (0.3) for more intense pixel corruption when the hacked effect is active
Prefer the full editor? Open it there →

📖 About This Sketch

Luna's creates a hypnotic rainbow spiral that expands outward and pulses with the energy of your voice or music. The sketch combines three advanced p5.js techniques: the p5.sound library for microphone capture and FFT frequency analysis, HSB color mode for smooth rainbow hues, and trigonometric math (sine, cosine) to generate the spiral's shape. The result is a stunning real-time music visualization that responds instantly to sound levels.

The code is organized around two visual modes: a default rainbow spiral and a toggle-able 'hacked screen' glitch effect. You'll learn how to capture audio input, analyze its frequency content with FFT, map that data to visual properties like wave amplitude and color, and switch between entirely different rendering modes. The sketch also demonstrates p5.speech for text-to-speech output, adding an extra interactive layer.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, initializes microphone input with p5.AudioIn, connects an FFT frequency analyzer to that input, and creates a speech synthesizer object. The userStartAudio() call prompts the browser to request microphone permissions.
  2. Every frame, draw() calls fft.analyze() to measure the current audio energy, then maps that energy level to a wave offset value between 0 and 30 pixels.
  3. If the hacked effect is inactive (the default), a spiral is drawn: a loop runs 4000 times, each iteration calculating a point further out from the center using angle and radius, with the radius wobbling up and down based on the wave offset from the music.
  4. The hue of each line segment is mapped from the angle around the spiral (0 to 360 degrees), creating a rainbow effect that stays fixed as the spiral rotates and expands.
  5. If the 'H' key is pressed, isHackedEffectActive toggles true, and the next frame draws random lines and glitchy pixel noise instead, creating a fake 'computer hacked' aesthetic.
  6. Pressing 'A' triggers text-to-speech, speaking a message about the visualization—demonstrating how p5.speech outputs audio alongside the music input.

🎓 Concepts You'll Learn

Audio input and microphone captureFFT frequency analysisReal-time data mappingTrigonometric spiral generationHSB color mode and hue mappingParametric curvesConditional rendering modesText-to-speech synthesisPixel manipulationKeyboard interaction

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch loads. It's where you initialize all global state: the canvas, audio devices, and any objects you'll use throughout the sketch. Everything here must happen before draw() starts running.

function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSB, 360, 100, 100); // Set color mode to HSB for rainbow

  mic = new p5.AudioIn(); // Create an audio input object
  mic.start(); // Start listening to the microphone

  fft = new p5.FFT(); // Create an FFT object to analyze frequencies
  fft.setInput(mic); // Connect FFT to the microphone

  // Create the speech object
  speech = new p5.Speech();
  speech.setLang('en-US'); // Set the language for speech synthesis
  speech.setRate(1.2);    // Set speech rate (1.0 is normal)
  speech.setPitch(1.0);   // Set speech pitch (1.0 is normal)

  userStartAudio(); // Prompt the user for microphone access. This is essential!
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Audio Input Initialization mic = new p5.AudioIn(); mic.start();

Creates an audio input object and starts listening to the microphone

calculation FFT Analyzer Setup fft = new p5.FFT(); fft.setInput(mic);

Creates a Fast Fourier Transform analyzer and connects it to the microphone input

calculation Speech Synthesizer Setup speech = new p5.Speech(); speech.setLang('en-US'); speech.setRate(1.2); speech.setPitch(1.0);

Creates a text-to-speech object and configures its language, speaking rate, and pitch

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the spiral responsive to screen size
colorMode(HSB, 360, 100, 100);
Switches to HSB (Hue-Saturation-Brightness) color mode instead of RGB, making it easy to create smooth rainbow gradients by varying hue from 0 to 360
mic = new p5.AudioIn();
Creates a new AudioIn object that will capture microphone input from the user's device
mic.start();
Activates the microphone so it begins recording audio input
fft = new p5.FFT();
Creates a Fast Fourier Transform analyzer that breaks audio down into frequency bands
fft.setInput(mic);
Connects the FFT analyzer to the microphone so it analyzes the live audio being captured
speech = new p5.Speech();
Creates a text-to-speech synthesizer object that can speak text aloud
speech.setLang('en-US');
Sets the speech language to American English
speech.setRate(1.2);
Sets the speaking rate to 1.2 times normal speed (slightly faster than natural speech)
speech.setPitch(1.0);
Keeps the pitch at normal level (1.0 is the baseline)
userStartAudio();
Prompts the browser to request microphone permissions from the user—this is essential before any audio input can work

draw()

draw() runs 60 times per second, constantly updating the visualization. The if/else structure lets you switch between two completely different rendering modes with a single boolean. Inside the spiral section, you see how to combine polar-to-Cartesian coordinate conversion (cos and sin), audio analysis mapping, and vertex() to build a complex parametric curve.

🔬 This loop draws 4000 points. What happens if you change the stroke saturation (second parameter) from 100 to 30? What about changing brightness from 80 to 20?

    for (let i = 0; i < 4000; i++) { 
      let hue = map(angle, 0, TWO_PI, 0, 360) % 360; 
      let brightness = map(radius, 0, max(width, height) / 2, 80, 100);
      stroke(hue, 100, brightness);

🔬 This line creates the wavy wobble using sin(angle * 10). What if you change 10 to 2? To 30? How does that change the spiral's wave pattern?

      let currentRadius = radius + sin(angle * 10) * waveOffset;
function draw() {
  background(0); // Black background for contrast

  if (isHackedEffectActive) {
    // === HACKED SCREEN EFFECT START ===
    stroke(100, 100, 100); // Bright green
    for (let i = 0; i < 50; i++) {
      line(random(width), random(height), random(width), random(height));
    }

    textSize(24);
    fill(100, 100, 100); // Bright green text
    textFont('monospace'); // Use a monospace font for code aesthetic
    textAlign(LEFT, TOP);
    text("ACCESS DENIED\nRE-ATTEMPTING PROTOCOL 7.4.2\nENCRYPTED DATA STREAM INITIATED\nTARGET IP: 192.168.1.1\nSTATUS: ONLINE\nSCANNING FOR VULNERABILITIES...", 20, 20);

    loadPixels();
    for (let i = 0; i < pixels.length; i += 4) {
      if (random() < 0.1) { // 10% chance to alter a pixel
        pixels[i] = random(50, 150);   // Red (random green hue for noise)
        pixels[i + 1] = random(50, 150); // Green
        pixels[i + 2] = random(50, 150); // Blue
      }
    }
    updatePixels();
    // === HACKED SCREEN EFFECT END ===
  } else {
    // === RAINBOW MUSIC WAVE START ===
    noFill();      // No fill for the spiral segments

    let centerX = width / 2;
    let centerY = height / 2;

    let radius = 0;
    let angle = 0;
    let angleIncrement = 0.05; // Density of the spiral
    let radiusIncrement = 0.25; // How quickly the spiral expands

    fft.analyze();
    let midEnergy = fft.getEnergy("mid"); 
    let waveOffset = map(midEnergy, 0, 255, 0, 30); // Max wave offset of 30 pixels

    beginShape();
    for (let i = 0; i < 4000; i++) { 
      let hue = map(angle, 0, TWO_PI, 0, 360) % 360; 
      let brightness = map(radius, 0, max(width, height) / 2, 80, 100);
      stroke(hue, 100, brightness); // Set the stroke color

      let currentRadius = radius + sin(angle * 10) * waveOffset; 
      
      let x = centerX + currentRadius * cos(angle);
      let y = centerY + currentRadius * sin(angle);

      vertex(x, y);

      angle += angleIncrement;
      radius += radiusIncrement;

      if (radius > max(width, height) / 2) {
        break;
      }
    }
    endShape();
    // === RAINBOW MUSIC WAVE END ===
  }
}
Line-by-line explanation (23 lines)

🔧 Subcomponents:

for-loop Random Glitch Lines for (let i = 0; i < 50; i++) { line(random(width), random(height), random(width), random(height)); }

Draws 50 random lines across the canvas to create a chaotic hacked aesthetic

for-loop Pixel Glitch Corruption for (let i = 0; i < pixels.length; i += 4) { if (random() < 0.1) { pixels[i] = random(50, 150); pixels[i + 1] = random(50, 150); pixels[i + 2] = random(50, 150); } }

Randomly corrupts 10% of pixels each frame with random RGB values to create a flickering glitch effect

calculation Audio Frequency Analysis fft.analyze(); let midEnergy = fft.getEnergy("mid"); let waveOffset = map(midEnergy, 0, 255, 0, 30);

Analyzes the current audio frequency, extracts mid-range energy, and maps it to a visual wave offset value

for-loop Spiral Point Generation for (let i = 0; i < 4000; i++) { let hue = map(angle, 0, TWO_PI, 0, 360) % 360; let brightness = map(radius, 0, max(width, height) / 2, 80, 100); stroke(hue, 100, brightness); let currentRadius = radius + sin(angle * 10) * waveOffset; let x = centerX + currentRadius * cos(angle); let y = centerY + currentRadius * sin(angle); vertex(x, y); angle += angleIncrement; radius += radiusIncrement; if (radius > max(width, height) / 2) break; }

Loops 4000 times to generate and draw all points of the spiral, each with hue from angle, brightness from radius, and waviness from audio energy

background(0);
Clears the canvas with black every frame, preventing motion trails and ensuring a clean slate for new drawings
if (isHackedEffectActive) {
Checks the toggle variable; if true, draws the glitch effect instead of the spiral
stroke(100, 100, 100);
Sets the stroke color to a bright green in HSB mode (hue 100, saturation and brightness both 100)
for (let i = 0; i < 50; i++) {
Loops 50 times to draw 50 random lines
line(random(width), random(height), random(width), random(height));
Draws a line from a random point to another random point; repeated 50 times creates visual chaos
textSize(24);
Sets the font size for the hacked text to 24 pixels
textFont('monospace');
Uses a monospace font (like code) to reinforce the 'hacked computer' theme
loadPixels();
Loads the current pixel data from the canvas into memory so it can be modified
for (let i = 0; i < pixels.length; i += 4) {
Loops through every pixel (incrementing by 4 because each pixel has R, G, B, and Alpha channels)
if (random() < 0.1) {
Has a 10% chance per pixel to apply the glitch effect, creating random flickering corruption
updatePixels();
Applies the modified pixel data back to the canvas, making the changes visible
fft.analyze();
Analyzes the current microphone input and breaks it into frequency bands
let midEnergy = fft.getEnergy("mid");
Extracts the energy level of mid-range frequencies (vocals and most instruments) from 0 to 255
let waveOffset = map(midEnergy, 0, 255, 0, 30);
Converts the audio energy (0–255) to a wave wobble amount (0–30 pixels); louder = more wobble
let hue = map(angle, 0, TWO_PI, 0, 360) % 360;
Maps the spiral's angle (0 to 2π radians) to hue values (0 to 360 degrees), creating a smooth rainbow around the spiral
let brightness = map(radius, 0, max(width, height) / 2, 80, 100);
Maps radius (how far from center) to brightness so the outer spiral is brighter than the inner parts
let currentRadius = radius + sin(angle * 10) * waveOffset;
Wobbles the radius using a sine wave; waveOffset from audio controls how much it wiggles, creating the music-reactive effect
let x = centerX + currentRadius * cos(angle);
Converts polar coordinates (angle and radius) to Cartesian x-coordinate using cosine
let y = centerY + currentRadius * sin(angle);
Converts polar coordinates to Cartesian y-coordinate using sine
vertex(x, y);
Adds this point to the current shape being drawn by beginShape() and endShape()
angle += angleIncrement;
Advances the angle slightly to move around the spiral; smaller increments = denser spiral
radius += radiusIncrement;
Increases the radius to spiral outward; larger increments = faster expansion
if (radius > max(width, height) / 2) {
Stops drawing when the spiral has expanded far enough to reach beyond the canvas, preventing wasted computation

keyPressed()

keyPressed() is called once every time the user presses a key. Use it to detect specific keys (by comparing the key variable) and trigger events like toggling modes or playing sounds. Notice the `!` operator flips boolean values—it's a concise way to toggle state without writing if/else.

🔬 This code toggles between two modes. What would happen if you removed the background(0) line inside the second if statement? Try removing it and predict what you'd see when you switch back from hacked mode to spiral mode.

  if (key === 'h' || key === 'H') { // Toggle hacked effect
    isHackedEffectActive = !isHackedEffectActive;
    if (!isHackedEffectActive) {
      background(0);
    }
  }
function keyPressed() {
  if (key === 'h' || key === 'H') { // Toggle hacked effect
    isHackedEffectActive = !isHackedEffectActive;
    if (!isHackedEffectActive) {
      background(0);
    }
  } else if (key === 'a' || key === 'A') { // New: Make it talk
    if (!speech.speaking()) { // Only speak if not already speaking
      speech.speak("I am the rainbow music wave. Press H for a different experience. Stay creative!");
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Toggle Hacked Effect if (key === 'h' || key === 'H') { isHackedEffectActive = !isHackedEffectActive; if (!isHackedEffectActive) { background(0); } }

Detects H key press, flips the hacked effect boolean, and clears the canvas when returning to spiral mode

conditional Trigger Text-to-Speech else if (key === 'a' || key === 'A') { if (!speech.speaking()) { speech.speak("I am the rainbow music wave. Press H for a different experience. Stay creative!"); } }

Detects A key press and speaks a message if no speech is already playing

if (key === 'h' || key === 'H') {
Checks if the user pressed either lowercase 'h' or uppercase 'H' key
isHackedEffectActive = !isHackedEffectActive;
Flips the toggle: if it was true, it becomes false; if it was false, it becomes true. The ! is the NOT operator.
if (!isHackedEffectActive) {
If we just switched the hacked effect OFF, clear the screen with a fresh black background
background(0);
Clears the canvas to black, removing any leftover glitch graphics from the previous frame
} else if (key === 'a' || key === 'A') {
Otherwise, checks if the user pressed A key instead
if (!speech.speaking()) {
Checks if the speech synthesizer is not currently speaking; prevents overlapping voices if the user presses A rapidly
speech.speak("I am the rainbow music wave. Press H for a different experience. Stay creative!");
Tells the speech synthesizer to speak this message aloud using text-to-speech

windowResized()

windowResized() is an event handler that runs automatically whenever the browser window is resized. This sketch uses it to keep the canvas fullscreen and to immediately redraw the hacked effect so it fills the new dimensions. Without it, resizing would leave empty space or require waiting for the next frame.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  if (isHackedEffectActive) {
    draw();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Canvas Resize Operation resizeCanvas(windowWidth, windowHeight);

Updates the canvas dimensions to match the current window size

conditional Redraw Hacked Effect if (isHackedEffectActive) { draw(); }

If hacked effect is active, immediately redraws it to fill the resized canvas

resizeCanvas(windowWidth, windowHeight);
Updates the canvas size to match the current window dimensions, handling browser window resize events
if (isHackedEffectActive) {
Checks if the hacked screen effect is currently active
draw();
Manually calls draw() once to immediately re-render the hacked effect at the new canvas size, preventing blank areas

📦 Key Variables

mic p5.AudioIn object

Captures live microphone audio input from the user's device; passed to the FFT analyzer to extract frequency data

let mic;
mic = new p5.AudioIn();
mic.start();
fft p5.FFT object

Analyzes the microphone audio and extracts frequency bands (like bass, mid, treble); provides energy values that drive the spiral's visual response

let fft;
fft = new p5.FFT();
fft.setInput(mic);
isHackedEffectActive boolean

Toggles between the rainbow spiral (false) and glitchy hacked screen (true); controlled by pressing H key

let isHackedEffectActive = false;
speech p5.Speech object

Text-to-speech synthesizer that speaks messages aloud; triggered by pressing A key

let speech;
speech = new p5.Speech();
centerX number

The x-coordinate of the spiral's center (middle of the canvas), used to anchor the spiral in place

let centerX = width / 2;
centerY number

The y-coordinate of the spiral's center (middle of the canvas), used to anchor the spiral in place

let centerY = height / 2;
radius number

Tracks the current distance from the spiral's center as it loops; increases each iteration to spiral outward

let radius = 0;
radius += radiusIncrement;
angle number

Tracks the current rotation angle (in radians) as the spiral loops; maps to hue for rainbow coloring

let angle = 0;
angle += angleIncrement;
angleIncrement number

How much the angle increases per loop iteration; smaller values pack more points tightly, larger values create sparser spirals

let angleIncrement = 0.05;
radiusIncrement number

How much the radius increases per loop iteration; controls how quickly the spiral expands outward

let radiusIncrement = 0.25;
waveOffset number

The amount of wobble applied to the spiral based on microphone energy (0–30 pixels); makes the spiral pulse with music

let waveOffset = map(midEnergy, 0, 255, 0, 30);
midEnergy number

The energy level of mid-range audio frequencies (0–255); extracted from FFT analysis and mapped to waveOffset

let midEnergy = fft.getEnergy("mid");
hue number

The current color value (0–360 degrees in HSB) for the spiral stroke; mapped from angle to create rainbow effect

let hue = map(angle, 0, TWO_PI, 0, 360) % 360;
brightness number

The current brightness value (80–100 in HSB) for the spiral stroke; increases toward the outer spiral for contrast

let brightness = map(radius, 0, max(width, height) / 2, 80, 100);
currentRadius number

The wobbled radius at each spiral point; calculated by adding a sine-wave wobble to the base radius

let currentRadius = radius + sin(angle * 10) * waveOffset;
x number

The x-coordinate of each spiral point; converted from polar coordinates using cosine

let x = centerX + currentRadius * cos(angle);
y number

The y-coordinate of each spiral point; converted from polar coordinates using sine

let y = centerY + currentRadius * sin(angle);

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() - hacked effect pixel loop

The hacked effect modifies pixel data but does not use map() for RGB values, causing potential values outside 0-255 range. The comment also incorrectly labels green hue modification.

💡 Use constrain() to ensure RGB values stay within 0-255: pixels[i] = constrain(random(50, 150), 0, 255). Also, correct the comment or clarify that random() already returns 50-150 for RGB.

PERFORMANCE draw() - pixel manipulation loop

Modifying pixels.length (typically millions of values) every frame is expensive; the 10% random check still touches every pixel

💡 Instead of looping through all pixels, randomly select a few thousand pixel indices and corrupt only those: for (let i = 0; i < 5000; i++) { let idx = floor(random(pixels.length / 4)) * 4; ... }

STYLE setup() - speech configuration

Speech rate and pitch are hardcoded as parameters; a beginner might not know these are tunable without reading the documentation

💡 Add comments next to setRate() and setPitch() explaining the valid ranges (e.g., 0.5–2.0 for rate) to make experimentation obvious

FEATURE draw() - spiral audio analysis

Only mid-range frequencies are used for the wobble; lower frequencies (bass) are ignored despite being visually interesting

💡 Add separate bass and treble energy variables and use them to control different aspects: let bassEnergy = fft.getEnergy("bass"); let trebleEnergy = fft.getEnergy("treble"); let radiusChangeFromBass = map(bassEnergy, 0, 255, 0.1, 0.5);

BUG keyPressed()

speech.speak() is called without checking if the speech object is properly initialized or if audio context is active; could fail silently

💡 Add a guard check: if (!speech || typeof speech.speak !== 'function') return; before calling speak()

STYLE draw() - hacked effect text

Hardcoded text messages and coordinates make it difficult to update or customize the hacked aesthetic

💡 Store the hacked screen text in a variable at the top of the file (e.g., const hackedText = '...') and reference it in draw(), then adjust x and y positions in one place

🔄 Code Flow

Code flow showing setup, draw, keypressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> audioinit[audio-init] audioinit --> fftsetup[fft-setup] fftsetup --> speechsetup[speech-setup] setup --> draw[draw loop] draw --> drawloop[Draw Loop] drawloop --> togglehacked[toggle-hacked] drawloop --> triggerspeech[trigger-speech] drawloop --> spiralanalysis[spiral-audio-analysis] spiralanalysis --> spiralloop[spiral-loop] spiralloop --> drawloop drawloop --> hackedrandomlines[hacked-random-lines] hackedrandomlines --> hackedpixelnoise[hacked-pixel-noise] hackedpixelnoise --> drawloop draw --> windowresized[windowresized] windowresized --> canvasresize[canvas-resize] canvasresize --> redrawhacked[redraw-hacked] redrawhacked --> drawloop click setup href "#fn-setup" click audioinit href "#sub-audio-init" click fftsetup href "#sub-fft-setup" click speechsetup href "#sub-speech-setup" click draw href "#fn-draw" click togglehacked href "#sub-toggle-hacked" click triggerspeech href "#sub-trigger-speech" click spiralanalysis href "#sub-spiral-audio-analysis" click spiralloop href "#sub-spiral-loop" click hackedrandomlines href "#sub-hacked-random-lines" click hackedpixelnoise href "#sub-hacked-pixel-noise" click windowresized href "#fn-windowresized" click canvasresize href "#sub-canvas-resize" click redrawhacked href "#sub-redraw-hacked"

❓ Frequently Asked Questions

What visual effects does the Luna's sketch create?

The sketch generates a dynamic visual display featuring a black background, bright green lines, and text resembling a hacked screen effect, with random pixel alterations for added noise.

How can users interact with the Luna's creative coding sketch?

Users can interact with the sketch by granting microphone access, which allows the audio input to influence the visual effects and speech synthesis.

What creative coding techniques are demonstrated in the Luna's sketch?

The sketch showcases audio analysis using FFT, real-time pixel manipulation, and the integration of speech synthesis to enhance the interactive experience.

Preview

Luna's - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Luna's - Code flow showing setup, draw, keypressed, windowresized
Code Flow Diagram