Music_Visualization

Music Visualization

https://jeremyluna.github.io/Music_Visualization/

A single-page music visualization app. Upload an audio file, play it in the browser, split the canvas layout, and choose or customize visualizers for each canvas.

The current implementation lives in clojure/ and is built with ClojureScript, Shadow-cljs, Reagent, the Web Audio API, and canvas rendering. The older vanilla JavaScript version is archived under archived/vanilla_js/.

Quickstart

Prerequisites

The Node/npm toolchain is pinned in clojure/.nvmrc, clojure/.node-version, clojure/.npmrc, and clojure/package.json. If you use nvm, run:

cd clojure
nvm install
nvm use

Check versions with:

node --version
npm --version
java -version

Install

cd clojure
npm ci

Use npm ci for normal setup and builds. It installs exactly from package-lock.json, so development and release builds use the same dependency tree on each machine.

Run Locally

Start the Shadow-cljs watcher:

cd clojure
nvm use
npm run dev

In another terminal, serve public/:

cd clojure
npm run serve:dev

Open http://localhost:8000.

Shadow-cljs also starts its own development server at http://127.0.0.1:3000, but the current npm serve script uses the static Python server on port 8000.

Features

Project Structure

clojure/
|-- deps.edn                    # Clojure/ClojureScript dependencies and aliases
|-- .nvmrc                      # Preferred Node.js version for nvm
|-- .node-version               # Preferred Node.js version for asdf/mise/fnm
|-- .npmrc                      # Enforces Node/npm engine checks during install
|-- package.json                # npm scripts and JS dependencies
|-- package-lock.json           # Locked npm dependency tree for reproducible installs
|-- shadow-cljs.edn             # Shadow-cljs browser build configuration
|-- public/
|   |-- index.html              # Browser entry point
|   |-- sample_processor.js     # AudioWorklet processor for sample capture
|   `-- js/                     # Shadow-cljs output (generated)
`-- src/
    |-- app/
    |   |-- core.cljs           # Root Reagent component
    |   |-- init.cljs           # App startup, audio init, and render loop startup
    |   `-- state.cljs          # Central app-state atom and dispatch actions
    |-- audio/
    |   |-- interop.cljs        # Web Audio, Canvas 2D, FileReader, and FFT wrappers
    |   |-- player.cljs         # AudioPlayer record and playback/file APIs
    |   `-- sample_puller.cljs  # AudioWorklet sample buffering
    |-- canvas/
    |   |-- model.cljs          # Pure layout tree operations
    |   |-- view.cljs           # Reagent canvas/split layout components
    |   `-- controller.cljs     # Convenience dispatch wrappers
    |-- ui/
    |   `-- control_panel.cljs  # File, playback, volume, and visualizer settings UI
    `-- visualizers/
        |-- protocol.cljs       # IVisualizer protocol
        |-- engine.cljs         # Render loop and visualizer instance lifecycle
        |-- registry.cljs       # Visualizer factory and metadata registry
        |-- stft.cljs           # FFT/STFT-style frequency visualizer
        `-- waveform.cljs       # Time-domain waveform visualizer

Script Reference

Development

Browser REPL

Start the watcher first:

cd clojure
npm run dev

Then connect to the app REPL:

npx shadow-cljs cljs-repl app

Useful REPL snippets:

(require '[app.state :as state])
@state/app-state
(state/dispatch :toggle-control-panel)

Production Build

cd clojure
nvm use
npm run release

The compiled output is written to public/js. To serve the release bundle locally:

npm run serve:release

Troubleshooting

App Does Not Load Or Shows “Loading App…”

  1. Check the browser console for errors.
  2. Confirm the dev server is running:

    curl http://localhost:8000
    
  3. Confirm clojure/public/js/main.js exists and has content.

“Cannot GET /js/main.js”

The dev server is not running, or it is serving from the wrong directory. Run this from clojure/:

npm run serve:dev

REPL Connection Fails

Kill any existing Shadow-cljs watch processes, then start fresh:

npx shadow-cljs watch app

Architecture

State

The app keeps browser-facing mutable state in one Reagent atom:

State changes flow through app.state/dispatch, while pure layout behavior lives in canvas.model.

Audio

audio.player/create-audio-player builds the browser audio graph:

HTMLAudioElement -> MediaElementAudioSourceNode -> GainNode -> speakers
                                                   `--------> AudioWorklet sample capture

Loaded audio files are played through the hidden audio element. The worklet posts sample frames to audio.sample-puller, which keeps per-channel circular buffers for visualizers.

Canvas Layout

Canvas layout is represented as a recursive tree of :canvas leaves and :split containers. Splitting a canvas replaces the selected leaf with a split node containing the original canvas and a new waveform canvas. Removing a canvas promotes its sibling so the layout remains valid.

Visualizers

Visualizers implement visualizers.protocol/IVisualizer:

The render loop in visualizers.engine walks the active layout, creates or reuses visualizer instances, syncs settings, and renders each mounted canvas on every animation frame.

Adding a Visualizer

  1. Add a new namespace under clojure/src/visualizers/.
  2. Implement visualizers.protocol/IVisualizer.
  3. Define a theme-settings function if the visualizer should derive default colors from the active theme.
  4. Add the visualizer metadata, factory, and theme settings function to visualizers.registry/visualizer-registry.
  5. Add settings controls in ui.control-panel/visualizer-settings if the visualizer has configurable options.

Todo List

Resources