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/.
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
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.
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.
app.initapp.state/app-state atom with dispatch-based updatespublic/sample_processor.jsrequestAnimationFrameIVisualizer protocol with waveform and STFT visualizer implementationsclojure/
|-- 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
npm run dev: Watch and recompile the browser build.npm run release: Build optimized production output into public/js.npm run serve:dev: Serve public/ on port 8000.npm run serve:release: Build release output, then serve it.npm run serve: Alias for serve:dev.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)
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
Confirm the dev server is running:
curl http://localhost:8000
clojure/public/js/main.js exists and has content.The dev server is not running, or it is serving from the wrong directory. Run this from clojure/:
npm run serve:dev
Kill any existing Shadow-cljs watch processes, then start fresh:
npx shadow-cljs watch app
The app keeps browser-facing mutable state in one Reagent atom:
:audio: audio context, player, sample puller, playback state, duration, current time, and volume:layout: the immutable canvas layout tree and next canvas ID:ui: control panel state:visualizers: active visualizer instances and settings:canvas-elements: mounted canvas DOM nodes keyed by canvas ID:samples: sample storage hooks for future expansionState changes flow through app.state/dispatch, while pure layout behavior lives in canvas.model.
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 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 implement visualizers.protocol/IVisualizer:
render: draw the current frame to a canvasupdate-settings: apply per-canvas settingsget-settings: expose current settingsThe 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.
clojure/src/visualizers/.visualizers.protocol/IVisualizer.theme-settings function if the visualizer should derive default colors from the active theme.visualizers.registry/visualizer-registry.ui.control-panel/visualizer-settings if the visualizer has configurable options.