xesled.ai synth pad

This sketch creates an interactive 4×4 grid of colorful musical pads that play different notes when clicked. Each pad lights up on tap and triggers a sine wave oscillator with a unique frequency from a chromatic scale, creating a playable synthesizer interface.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the oscillator waveform — Switching from 'sine' to 'square' or 'sawtooth' creates bright, buzzy synthesizer tones instead of smooth sine waves
  2. Make notes play longer — Increasing the timeout value from 200 to 800 milliseconds makes each tap sustain much longer, creating pad-like sounds
  3. Make pads bigger and further apart — Increasing padSpacing spreads pads farther apart and makes them larger, creating a more spacious layout
  4. Make the background dark — Changing the background number from 220 (light gray) to 20 (dark gray) creates a high-contrast dark theme
  5. Transpose the scale down — Lowering the baseFrequency from 261.63 Hz (C4) to 130.81 Hz (C3) plays all notes an octave lower
  6. Make the fade-out much smoother — Increasing the fade-out time from 0.2 to 1 second creates a long, ghostly decay instead of a sharp cutoff
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a playable musical instrument from a 4×4 grid of glowing squares. When you click any pad, it lights up brighter and plays its own musical note using p5.sound's oscillators. The sketch combines three powerful p5.js tools: a 2D grid layout system to arrange the pads, oscillators to generate sine wave tones at different frequencies, and the mouse event system to respond to user input.

The code is organized into setup (which creates 16 oscillators at chromatic scale frequencies), draw (which renders the grid and highlights active pads), and mouseClicked (which plays the sound and manages the visual feedback). By studying this sketch you will learn how to build interactive grids, manage sound synthesis with p5.sound, and coordinate visuals with audio using state tracking.

⚙️ How It Works

  1. When the sketch loads, setup() creates a responsive canvas that fills the window, generates 16 oscillators tuned to a chromatic scale starting at C4, assigns each pad a unique color using HSB mode, and starts all oscillators at zero volume (silent).
  2. Every frame, draw() clears the background and renders the 4×4 grid by looping through each pad position, filling it with its assigned color, and brightening the color if that pad is currently playing.
  3. When you click on the canvas, mouseClicked() calculates which grid cell was clicked by dividing the mouse position by the pad size, finds the corresponding oscillator, and fades its volume up to 0.5 over 50 milliseconds, making it audible.
  4. A setTimeout timer automatically fades that oscillator back to silence after 200 milliseconds, creating a short 'pluck' sound effect and dimming the visual highlight on the pad.
  5. The activePads 2D array tracks which pads are currently playing, so the draw loop knows which squares to brighten each frame.

🎓 Concepts You'll Learn

2D grid layoutOscillators and sine wavesChromatic scaleMouse interactionState tracking with 2D arraysHSB color modeAudio amplitude and timingResponsive canvas

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It prepares all 16 oscillators with their frequencies, assigns each pad a color, and initializes the state tracking array. The chromatic scale formula (baseFreq × 2^(noteIndex/12)) is the mathematical foundation of Western music tuning.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Tip: Use windowWidth/windowHeight for responsive canvas

  // Enable audio playback. This is crucial for p5.sound.
  // userStartAudio() must be called in response to a user gesture
  // (like a mouse click or key press) to bypass browser autoplay policies.
  userStartAudio();

  // Calculate pad size based on window dimensions and desired spacing
  padSizeX = (width - (cols + 1) * padSpacing) / cols;
  padSizeY = (height - (rows + 1) * padSpacing) / rows;

  // Generate musical frequencies (e.g., a chromatic scale over two octaves)
  // Starting from C4 (261.63 Hz)
  const baseFrequency = 261.63; // C4
  for (let i = 0; i < rows * cols; i++) {
    // Each pad corresponds to a note in a chromatic scale
    // frequency = baseFreq * 2^(noteIndex / 12)
    const freq = baseFrequency * pow(2, i / 12);
    frequencies.push(freq);

    // Create a sine oscillator for each frequency
    let osc = new p5.Oscillator('sine'); // 'sine', 'triangle', 'sawtooth', 'square'
    osc.freq(freq);
    osc.amp(0); // Start with 0 amplitude (silent)
    osc.start(); // Start the oscillator, but it won't be audible yet
    oscillators.push(osc);
  }

  // Generate colorful pads using HSB color mode
  colorMode(HSB, 360, 100, 100);
  for (let i = 0; i < rows * cols; i++) {
    // Vary hue for different colors
    padColors.push(color(i * (360 / (rows * cols)), 80, 90));
  }
  colorMode(RGB, 255); // Switch back to RGB for background

  // Initialize activePads 2D array to all false
  for (let i = 0; i < rows; i++) {
    activePads[i] = [];
    for (let j = 0; j < cols; j++) {
      activePads[i][j] = false;
    }
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Chromatic Scale Frequency Loop for (let i = 0; i < rows * cols; i++) { const freq = baseFrequency * pow(2, i / 12); frequencies.push(freq); let osc = new p5.Oscillator('sine'); osc.freq(freq); osc.amp(0); osc.start(); oscillators.push(osc); }

Creates 16 oscillators tuned to consecutive chromatic notes; each frequency is calculated using the 12-tone equal temperament formula

for-loop Color Assignment Loop for (let i = 0; i < rows * cols; i++) { padColors.push(color(i * (360 / (rows * cols)), 80, 90)); }

Generates 16 unique colors distributed evenly across the hue spectrum using HSB mode

for-loop 2D Array Initialization for (let i = 0; i < rows; i++) { activePads[i] = []; for (let j = 0; j < cols; j++) { activePads[i][j] = false; } }

Creates a 4×4 2D array tracking which pads are currently playing (true) or silent (false)

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the grid responsive to screen size
userStartAudio();
Enables audio playback—this must be called in response to a user gesture (like the first click) to comply with browser autoplay policies
padSizeX = (width - (cols + 1) * padSpacing) / cols;
Calculates the width of each pad by subtracting spacing from total width and dividing by the number of columns
padSizeY = (height - (rows + 1) * padSpacing) / rows;
Calculates the height of each pad by subtracting spacing from total height and dividing by the number of rows
const freq = baseFrequency * pow(2, i / 12);
Calculates the frequency for each pad using the chromatic scale formula: each semitone multiplies the previous frequency by 2^(1/12) ≈ 1.0595
let osc = new p5.Oscillator('sine');
Creates a new oscillator that will generate a sine wave (smooth, pure tone)
osc.freq(freq);
Sets this oscillator's frequency to the calculated chromatic note frequency
osc.amp(0);
Sets the oscillator's initial amplitude to 0 (completely silent)—it won't make sound until amplitude increases
osc.start();
Starts the oscillator running in the background, but it remains silent because amplitude is 0
colorMode(HSB, 360, 100, 100);
Switches to HSB color mode to easily generate a rainbow of colors by varying hue
padColors.push(color(i * (360 / (rows * cols)), 80, 90));
Creates a color by distributing hues evenly across the 360-degree spectrum; each pad gets a different hue
colorMode(RGB, 255);
Switches back to RGB mode for the rest of the sketch

draw()

draw() runs 60 times per second, redrawing the entire grid. By checking activePads[i][j], it provides instant visual feedback when you tap a pad. The nested loop structure is the standard pattern for rendering 2D grids in p5.js.

🔬 These lines calculate where each pad appears on screen. What happens if you swap the calculations—put j in the y line and i in the x line? Predict which direction the grid will rotate or flip.

      // Calculate pad position
      let x = padSpacing + j * (padSizeX + padSpacing);
      let y = padSpacing + i * (padSizeY + padSpacing);

🔬 This code brightens active pads. What if you changed it to saturate the color instead—try setting s to 100 when the pad is active instead of keeping it the same? What visual effect does full saturation create?

      if (activePads[i][j]) {
        // Switch to HSB to easily brighten the color
        colorMode(HSB, 360, 100, 100);
        let h = hue(padColor);
        let s = saturation(padColor);
        let b = brightness(padColor);
        fill(h, s, min(100, b + 20)); // Increase brightness, cap at 100
        colorMode(RGB, 255); // Switch back to RGB
      } else {
        fill(padColor);
      }
function draw() {
  background(220); // Light gray background

  // Draw the 4x4 grid of pads
  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      // Calculate pad position
      let x = padSpacing + j * (padSizeX + padSpacing);
      let y = padSpacing + i * (padSizeY + padSpacing);

      // Determine pad color
      let padColor = padColors[i * cols + j];

      // If the pad is currently playing, make it brighter for visual feedback
      if (activePads[i][j]) {
        // Switch to HSB to easily brighten the color
        colorMode(HSB, 360, 100, 100);
        let h = hue(padColor);
        let s = saturation(padColor);
        let b = brightness(padColor);
        fill(h, s, min(100, b + 20)); // Increase brightness, cap at 100
        colorMode(RGB, 255); // Switch back to RGB
      } else {
        fill(padColor);
      }

      noStroke(); // No border for pads
      rect(x, y, padSizeX, padSizeY, 10); // Draw rectangle with rounded corners
    }
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Nested Grid Loop for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) {

Iterates through every row and column to draw all 16 pads on screen

conditional Active Pad Brightness Check if (activePads[i][j]) { colorMode(HSB, 360, 100, 100); let h = hue(padColor); let s = saturation(padColor); let b = brightness(padColor); fill(h, s, min(100, b + 20)); colorMode(RGB, 255); } else { fill(padColor); }

Checks if a pad is playing; if true, increases its brightness by 20 points for visual feedback; otherwise uses the normal color

background(220);
Clears the canvas with light gray, erasing everything drawn last frame—this must happen every frame or pads would leave trails
for (let i = 0; i < rows; i++) {
Outer loop iterates through each row (0, 1, 2, 3)
for (let j = 0; j < cols; j++) {
Inner loop iterates through each column (0, 1, 2, 3), creating a nested loop that visits all 16 positions
let x = padSpacing + j * (padSizeX + padSpacing);
Calculates the x position of the pad: starting at padSpacing, then adding the column index times (pad width plus spacing)
let y = padSpacing + i * (padSizeY + padSpacing);
Calculates the y position of the pad: starting at padSpacing, then adding the row index times (pad height plus spacing)
let padColor = padColors[i * cols + j];
Retrieves the color for this pad by converting its 2D grid position (i, j) to a 1D array index: i × 4 + j
if (activePads[i][j]) {
Checks if this pad is currently playing (true) or silent (false)
let h = hue(padColor);
Extracts the hue component of the pad's color in HSB mode
let b = brightness(padColor);
Extracts the brightness component of the pad's color in HSB mode
fill(h, s, min(100, b + 20));
Sets the fill color to the same hue and saturation but with brightness increased by 20, capped at 100 to prevent overflow
rect(x, y, padSizeX, padSizeY, 10);
Draws a rounded rectangle at position (x, y) with the calculated dimensions and a corner radius of 10 pixels

mouseClicked()

mouseClicked() is triggered every time you click the canvas. It converts pixel coordinates to grid indices, looks up the corresponding oscillator, and ramps its amplitude up then down to create an acoustic pluck sound. The setTimeout timer is key to automated note-off behavior—without it, the note would never stop playing.

🔬 This block creates a short 'pluck' sound. The 200ms delay controls sustain time. What happens if you change 200 to 1000? To 50? How does note length affect the musical feel when you play a melody?

    if (!activePads[gridY][gridX]) {
      // Set amplitude (volume) and play the tone
      osc.amp(0.5, 0.05); // Fade in to 0.5 amplitude over 0.05 seconds
      activePads[gridY][gridX] = true; // Mark pad as active

      // Stop the tone after a short delay (e.g., 200 milliseconds)
      setTimeout(() => {
        osc.amp(0, 0.2); // Fade out to 0 amplitude over 0.2 seconds
        activePads[gridY][gridX] = false; // Mark pad as inactive
      }, 200);
    }
function mouseClicked() {
  // Determine which pad was clicked
  let gridX = floor((mouseX - padSpacing) / (padSizeX + padSpacing));
  let gridY = floor((mouseY - padSpacing) / (padSizeY + padSpacing));

  // Check if the click is within the grid boundaries
  if (gridX >= 0 && gridX < cols && gridY >= 0 && gridY < rows) {
    let padIndex = gridY * cols + gridX;
    let osc = oscillators[padIndex];

    // Check if the oscillator is already playing
    if (!activePads[gridY][gridX]) {
      // Set amplitude (volume) and play the tone
      osc.amp(0.5, 0.05); // Fade in to 0.5 amplitude over 0.05 seconds
      activePads[gridY][gridX] = true; // Mark pad as active

      // Stop the tone after a short delay (e.g., 200 milliseconds)
      setTimeout(() => {
        osc.amp(0, 0.2); // Fade out to 0 amplitude over 0.2 seconds
        activePads[gridY][gridX] = false; // Mark pad as inactive
      }, 200);
    }
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Grid Position Calculation let gridX = floor((mouseX - padSpacing) / (padSizeX + padSpacing)); let gridY = floor((mouseY - padSpacing) / (padSizeY + padSpacing));

Converts pixel coordinates of the mouse click into grid column and row indices

conditional Boundary Validation if (gridX >= 0 && gridX < cols && gridY >= 0 && gridY < rows) {

Ensures the click landed inside the 4×4 grid, preventing crashes from out-of-bounds array access

conditional Audio Envelope and Timing if (!activePads[gridY][gridX]) { osc.amp(0.5, 0.05); activePads[gridY][gridX] = true; setTimeout(() => { osc.amp(0, 0.2); activePads[gridY][gridX] = false; }, 200); }

Plays the note by ramping amplitude up, then schedules an automatic fade-out after 200ms to create a pluck-like sound

let gridX = floor((mouseX - padSpacing) / (padSizeX + padSpacing));
Converts the mouse's x pixel position to a grid column number: subtract the left padding, divide by (pad width + spacing), and round down
let gridY = floor((mouseY - padSpacing) / (padSizeY + padSpacing));
Converts the mouse's y pixel position to a grid row number: subtract the top padding, divide by (pad height + spacing), and round down
if (gridX >= 0 && gridX < cols && gridY >= 0 && gridY < rows) {
Validates that the click is within the grid boundaries (columns 0-3, rows 0-3); prevents accessing out-of-bounds array elements
let padIndex = gridY * cols + gridX;
Converts the 2D grid position (gridY, gridX) to a 1D array index: row × 4 + column
let osc = oscillators[padIndex];
Retrieves the oscillator object assigned to the clicked pad
if (!activePads[gridY][gridX]) {
Checks if the pad is not already playing; the ! operator reverses the boolean (true becomes false, false becomes true)
osc.amp(0.5, 0.05);
Fades the oscillator's amplitude to 0.5 (50% volume) over 0.05 seconds (50 milliseconds), creating a quick attack
activePads[gridY][gridX] = true;
Sets the pad's state to true, marking it as currently playing so draw() will brighten it
setTimeout(() => {
Schedules a function to run after a delay (200 milliseconds), allowing the note to sustain briefly
osc.amp(0, 0.2);
Fades the oscillator's amplitude to 0 (silent) over 0.2 seconds (200 milliseconds), creating a smooth decay
activePads[gridY][gridX] = false;
Sets the pad's state to false, marking it as inactive so draw() will return it to normal brightness
}, 200);
Closes the setTimeout call with a delay of 200 milliseconds (0.2 seconds), controlling how long the note plays

windowResized()

windowResized() is a p5.js built-in function that fires automatically when the window is resized. This sketch uses it to keep the grid responsive, so the pads always fill the available space and remain clickable at any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Called when preview panel is resized

  // Recalculate pad size for responsiveness
  padSizeX = (width - (cols + 1) * padSpacing) / cols;
  padSizeY = (height - (rows + 1) * padSpacing) / rows;
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window dimensions whenever the browser window is resized or the preview panel changes size
padSizeX = (width - (cols + 1) * padSpacing) / cols;
Recalculates the pad width so pads scale proportionally to the new canvas width
padSizeY = (height - (rows + 1) * padSpacing) / rows;
Recalculates the pad height so pads scale proportionally to the new canvas height

📦 Key Variables

rows number

The number of rows in the grid (4), used to determine grid dimensions and loop bounds

const rows = 4;
cols number

The number of columns in the grid (4), used to determine grid dimensions and loop bounds

const cols = 4;
padSizeX number

The width of each pad in pixels, calculated in setup() based on canvas width and spacing

let padSizeX;
padSizeY number

The height of each pad in pixels, calculated in setup() based on canvas height and spacing

let padSizeY;
padSpacing number

The gap in pixels between adjacent pads, and between pads and the canvas edge

const padSpacing = 10;
oscillators array

Stores 16 p5.Oscillator objects, one for each pad, each tuned to a different frequency

let oscillators = [];
frequencies array

Stores the 16 frequency values in Hz for each oscillator, calculated from the chromatic scale

let frequencies = [];
padColors array

Stores 16 color objects, one for each pad, distributed evenly across the hue spectrum

let padColors = [];
activePads array

A 2D array (4×4) tracking which pads are currently playing (true) or silent (false), used by draw() to brighten active pads

let activePads = [];

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG mouseClicked() - double-tap protection

If you click a pad while it's already playing, the check !activePads[gridY][gridX] prevents a second sound, but multiple fast clicks in succession can create a stutter effect if timing is poor

💡 Add a small cooldown delay using millis() to prevent rapid re-triggering: only allow a new note if (millis() - lastClickTime) > 100

PERFORMANCE draw() - HSB color conversion

Every frame, active pads convert to HSB mode, extract hue/saturation/brightness, and convert back to RGB—this is inefficient for 16 pads per frame

💡 Pre-calculate brightened colors in setup() and store them in an array, then simply look them up in draw() without re-computing HSB conversions

FEATURE mouseClicked() - simultaneous notes

The check if (!activePads[gridY][gridX]) prevents overlapping sounds, so you cannot play chords by clicking multiple pads at once

💡 Remove or modify the check to allow multiple pads to play simultaneously, creating true polyphonic music instead of single-note playback

STYLE setup() - frequency array and oscillator array

The frequencies array is populated but never used; only the oscillators array matters, creating confusing unused code

💡 Either remove the frequencies array entirely, or use it for documentation by logging or displaying note names alongside frequencies

FEATURE oscillator setup

All oscillators run at full sample rate from the start, consuming CPU even though only one oscillator usually plays at a time

💡 Consider using AudioContext's sample-accurate scheduling or lazy-loading oscillators on first click to reduce baseline CPU usage

🔄 Code Flow

Code flow showing setup, draw, mouseclicked, windowresized

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

graph TD start[Start] --> setup[setup] setup --> frequencygeneration[Frequency Generation Loop] setup --> colorgeneration[Color Generation Loop] setup --> activepadsinit[Active Pads Initialization] setup --> draw[draw loop] click setup href "#fn-setup" click frequencygeneration href "#sub-frequency-generation" click colorgeneration href "#sub-color-generation" click activepadsinit href "#sub-activepads-init" draw --> gridloop[Grid Loop] draw --> activepadcheck[Active Pad Brightness Check] click draw href "#fn-draw" click gridloop href "#sub-grid-loop" click activepadcheck href "#sub-active-pad-check" gridloop --> rowloop[Row Loop] rowloop --> collop[Column Loop] collop --> activepadcheck collop --> draw mouseclicked[mouseClicked] --> gridclickcalculation[Grid Position Calculation] gridclickcalculation --> boundarycheck[Boundary Validation] boundarycheck --> audioenvelope[Audio Envelope and Timing] audioenvelope --> mouseclicked click mouseclicked href "#fn-mouseclicked" click gridclickcalculation href "#sub-grid-click-calculation" click boundarycheck href "#sub-boundary-check" click audioenvelope href "#sub-audio-envelope" windowresized[windowResized] --> draw click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual experience does the xesled.ai synth pad sketch provide?

The sketch presents a vibrant 4x4 grid of colorful pads that light up in response to user interaction, creating a dynamic display of color and sound.

How can users interact with the xesled.ai synth pad sketch to create music?

Users can tap on the colorful pads to trigger different musical notes, allowing them to experiment with various combinations to generate unique sound patterns.

What creative coding concepts are showcased in the xesled.ai synth pad sketch?

This sketch demonstrates the integration of sound synthesis with visual elements, showcasing how user interactions can influence both audio and visual outputs in a real-time environment.

Preview

xesled.ai synth pad - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of xesled.ai synth pad - Code flow showing setup, draw, mouseclicked, windowresized
Code Flow Diagram