WebMIDI + p5.js Examples

Music Devices — IM-UH 3116 / NYU Abu Dhabi

Four small browser pages showing how to read MIDI in the browser with WebMIDI.js and visualize it with p5.js. Built alongside the class hardware kit (ESP32-S3 Reverse TFT Feather + NeoKey 1x4 + NeoSlider) but any MIDI controller will work.

Requirements

Use Chrome or Edge — they have built-in Web MIDI support. Firefox needs the Jazz plugin.

Shared setup

All four examples are built from the same ingredients:

The pattern shared by every example: MIDI events update state; the render loop reads state. MIDI messages arrive at unpredictable times, but p5 draws on a fixed tick. Keeping the two decoupled is the single most important idea — it’s what stops the canvas from flashing, stuttering, or missing events when your controller gets chatty.

Examples

01 MIDI Monitor WebMIDI only

List all connected MIDI devices and watch a live log of every incoming message — notes, velocity, CC values. A useful debugging tool when building your own controllers.

Techniques
  • Listen broadly. Subscribes to named events (note on/off, control change, pitch bend) and a catch-all that fires on every raw MIDI byte — so messages the library doesn’t have a convenience event for still show up.
  • Cap the log. The message list is trimmed to the most recent 200 entries so a long session doesn’t slow the page down.
  • Colour-coded types. Notes, CCs, and everything else each get their own colour — so at a glance you can see which kinds of messages your controller is actually sending.
  • Filter dropdown. Narrow the view to one message type when you’re hunting a specific bug.

Why start here

Before you build any visualiser, you need to see what your controller is actually sending. Half the bugs in a later example are really surprises in the MIDI stream — a duplicated note-on, a CC on channel 2 when you expected channel 1. This page is your oscilloscope.

Source Code

02 Note Visualizer WebMIDI + p5.js

Press the four NeoKey buttons to spawn colorful ripples on a p5.js canvas. Each button maps to a position and color; velocity controls the ring size.

Techniques
  • Dynamic registration. No pre-configured mapping of note to colour or screen position. The first time a note is received, it’s given a hue and a slot on the screen.
  • Pitch-based colour. Each pitch class (C, C#, D, …) gets its own hue, rotating around the colour wheel by semitone. Two notes an octave apart share a colour; neighbouring notes look similar.
  • Layout computed at draw time. The currently-seen notes are sorted by pitch and spread evenly across the canvas every frame. A 3-note song and a 12-note song both get a clean layout with no hard-coded positions.
  • Velocity-scaled ripples. A harder hit produces a larger, longer-lived ring. Velocity is free expressive data — use it.
  • Readable labels. Note name in bold and MIDI number underneath, large enough to read from across the room.

Why this shape

The temptation with a “four-button visualiser” is to hard-code four positions. Resist it. If the same code handles 1 note or 20 with no config, you can reuse it with a keyboard, a drum pad, or a wind controller — whatever happens to be plugged in.

Source Codep5 editor

03 CC Visualizer WebMIDI + p5.js

Move the NeoSlider to see a live dial and scrolling waveform in p5.js. Shows how to read Control Change messages and map them to visual parameters.

Techniques
  • Keyed by channel + CC number. Channel 1 CC 74 and channel 2 CC 74 are tracked as separate streams, since they often come from different controllers or different knobs.
  • Named CCs. A small lookup table turns the raw number into a friendly label — CC 74 becomes “Filter Cutoff”, CC 1 becomes “Mod Wheel”, and so on. Unknown numbers just show as “CC 42”.
  • Two views of the same data. The dial is the instantaneous value (“where is it now?”); the waveform is history (“how has it been moving?”). Each answers a different question, so both earn their place on screen.
  • Grid of live chips. Every CC ever received is kept as a small card with an in-place value bar. Wiggle three knobs and you see three bars update simultaneously — no “select which one to watch” dropdown.

Why it works this way

Controllers rarely send a single CC. A synth knob bank, a mod wheel, a foot pedal, and a breath controller all at once is normal. A visualiser that assumes one-CC-at-a-time breaks the moment you plug in real gear. Build for many from the start.

Source Codep5 editor

04 Integrated p5 / CircuitPython WebMIDI + p5.js

Press keys to spawn glowing particle bursts; move the slider to change gravity. Notes and CC are both auto-detected. Demonstrates using NoteOn/Off events alongside a continuous CC value to control physics in real time.

Pairs with CircuitPython/midi_particles.py — the hardware sends the same notes (E minor pentatonic) and CC number, with button colours matching the on-screen particle hues.

Techniques
  • Gravity from CC. The continuous CC value is mapped to a gravity value that goes from strongly negative (float upward) through zero (weightless) to positive (fall). One knob, the whole behaviour range.
  • Chromatically tuned explosion sounds. Every note press fires a short synthesised burst whose pitch matches the MIDI note. Built from three stacked layers — a pitched “thump” that drops in frequency, a sub-octave body, and filtered noise for sparkle.
  • Particle interactions between different notes. Particles from the same note ignore each other. Particles from different notes push apart at close range and pull together at medium range, creating swirling orbits when multiple notes are held.
  • Connection sparks. When particles from different notes get very close, a short glowing arc is drawn between them — the system “notices” the interaction and makes it visible.
  • Fast enough at N particles. The example uses a spatial grid to only check nearby neighbours, keeping the frame rate smooth even with hundreds of particles on screen.

Source Codep5 editor

Running locally

These pages must be served over HTTP, not opened as a file. From inside the docs/ directory:

cd docs && python3 -m http.server 8000

Then open http://localhost:8000/p5examples.html.

Tips for generating similar projects with AI

Tip

These examples were built with Claude as a pair-programmer. The advice below is useful whether you start from scratch or copy one of these and diff your way to something new.

  1. Say what it should do, not just what it is. “A page where pressing a key spawns a coloured ripple that fades over ~1 second, and the ripple’s size grows with velocity” beats “a note visualiser”. Describe the behaviour a user would see — inputs, outputs, how things move, what changes over time. The model will fill in reasonable code; it can’t fill in your intent.
  2. Point the AI at an existing file for hardware setup. Instead of retyping pin numbers and I2C addresses, say “use the same hardware setup as CircuitPython/spritekeys.py” (or whichever file matches your rig). It will read it, pick up the NeoKey / NeoSlider init code, and match the firmware you already have working — no guessing about which addresses or which notes.
  3. Ask for one example at a time. Generating four visualisers in one prompt gives you four shallow visualisers. Generating them sequentially — each building on the last — gives you one that actually works and three that borrow from it.
  4. When something breaks, show the model the symptom, not your theory. “The dial draws only the outer arc, the center is blank” is more useful than “I think there’s a scope problem with dialR”. The model will find the real cause faster from the observable behaviour.
  5. Get it running first, then ask for changes. Before asking the AI to add features, make sure the basic version actually opens in your browser and responds to your controller. If the first version doesn’t work, a second round of “also add sound and particles” is going to pile new bugs on top of old ones. Small working step → small change → small working step.
  6. Ask the AI to explain anything you don’t understand. If it gives you code with Math.pow(2, (note - 69) / 12) or an hsvToRgb function and you’re not sure what those do, ask. “Explain this line” or “why did you choose this formula” is a legitimate prompt — you’ll learn the technique and be able to modify it yourself next time. Shipping code you can’t read means you’re stuck the moment it breaks.
  7. Read the code you commit. AI will confidently hand you code that uses a variable that doesn’t exist, or listens to a wrong event name. Open the file, skim it once, run it — don’t paste-and-ship.
  8. Tell the model what you’ve ruled out. “I already checked that WebMidi.enable() succeeds and the listener fires — the bug is in the render path” saves five rounds of diagnostic suggestions.