Sonora: local 3D audio visualizer (initial import)

Three.js + Web Audio FFT visualizer with Constellation, Texture, Frequency
and Modulation views. Vite build, node --test suite.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JABXBRLs9T6nqHKUVwoTHJ
This commit is contained in:
marcuspaico
2026-09-08 15:57:51 +10:00
commit ab748bfb9d
18 changed files with 2282 additions and 0 deletions

53
index.html Normal file
View File

@@ -0,0 +1,53 @@
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="theme-color" content="#070809"><meta name="description" content="Explore your music as a living 3D data sculpture. Local audio visualization with interactive spectral analysis."><title>Sonora — Sound in space</title></head>
<body>
<div id="app">
<header class="topbar">
<a class="brand" href="./" aria-label="Sonora home"><svg viewBox="0 0 36 36" aria-hidden="true"><path d="M3 18h4l3-10 5 22 6-26 5 22 3-8h4"/></svg><span>sonora<span class="brand-dot">.</span></span></a>
<span class="tagline">Sound, given dimension.</span>
<div class="top-actions"><span class="local"><span></span> Runs locally</span><button id="help" class="quiet" aria-label="About the visualization">?</button></div>
</header>
<main>
<aside class="sidebar">
<div class="intro"><p class="eyebrow">Audio observatory</p><h1>Listen. <br>Look closer.</h1><p>Every sound leaves a shape.<br>Explore the one inside yours.</p></div>
<button id="upload" class="upload"><span class="upload-icon">↥</span><strong>Open a song</strong><span>or drop a file anywhere</span><small>MP3, WAV, M4A, FLAC, MP4</small></button>
<input id="file" type="file" accept="audio/*,video/mp4,.mp3,.wav,.m4a,.flac,.ogg,.aac,.mp4,.webm" hidden>
<button id="demo" class="demo-button"><span>▷</span> Play birdsong <span class="duration">1:10</span></button>
<div class="settings">
<div class="section-title">Shape the view<button id="reset-settings" class="text-button">Reset</button></div>
<label class="range-label" for="sensitivity">Sensitivity <output id="sensitivity-value">1.0×</output></label><input id="sensitivity" type="range" min="0.5" max="3" step="0.1" value="1">
<label class="range-label" for="trail">Trail length <output id="trail-value">6 s</output></label><input id="trail" type="range" min="3" max="30" step="1" value="6">
<div class="range-label palette-label">Color palette <span id="palette-name">Ember</span></div>
<div class="palettes" role="group" aria-label="Color palette"><button data-palette="ember" class="swatch ember selected" aria-label="Ember palette" aria-pressed="true"></button><button data-palette="spectrum" class="swatch spectrum" aria-label="Spectrum palette" aria-pressed="false"></button><button data-palette="glacier" class="swatch glacier" aria-label="Glacier palette" aria-pressed="false"></button><button data-palette="orchid" class="swatch orchid" aria-label="Orchid palette" aria-pressed="false"></button></div>
<div class="appearance"><label class="mapping-label" for="color-mode">Colour by <select id="color-mode"><option value="position">Position in space</option><option value="frequency">Frequency</option><option value="amplitude">Loudness</option></select></label><p id="color-description">Colour follows position. Brightness follows sound.</p><label class="range-label" for="glow">Glow <output id="glow-value">1.35×</output></label><input id="glow" type="range" min="0.5" max="2.5" step="0.05" value="1.35"></div>
<label class="toggle-row" for="rotate">Orbit automatically <input id="rotate" type="checkbox" role="switch"><span class="switch"></span></label>
<label class="toggle-row" for="connections">Connect the dots <input id="connections" type="checkbox" role="switch" checked><span class="switch"></span></label>
<label class="toggle-row" for="labels">Floating measurements <input id="labels" type="checkbox" role="switch" checked><span class="switch"></span></label>
</div>
<p class="privacy">Your music stays on your device.<br>No uploads. Just sound and space.</p>
</aside>
<section class="workspace constellation-view" aria-label="Audio visualization">
<div class="workspace-heading"><div><span class="status-dot"></span><span id="status">Illustrative preview</span></div><div class="performance-info"><span id="sample-info">Waiting for sound</span><span id="fps" title="Measured rendering frames per second">— fps</span></div></div>
<div class="plots">
<article class="plot main-plot" id="main-plot">
<div class="plot-heading"><div><h2 id="main-title">Acoustic constellation</h2><p id="main-description">A living architecture of sound</p></div><div class="plot-actions"><button id="reset-camera" aria-label="Reset camera" title="Reset camera">↺</button><button id="fullscreen" aria-label="Toggle fullscreen" title="Fullscreen">⛶</button></div></div>
<div id="main-canvas" class="canvas-host" aria-label="Interactive 3D audio plot. Drag to rotate and scroll to zoom."></div>
<div class="scene-hud"><span>Amplitude <b id="hud-amplitude">—</b></span><span>Time <b id="hud-time">—</b></span></div>
<div class="scene-caption"><span id="node-count">0 points</span><span>Labels: amplitude / frequency</span></div>
<div class="plot-footer"><span id="main-axis">Frequency, texture &amp; amplitude · feature projection</span><span class="legend"><i></i> position → colour</span></div><div class="camera-hint">Drag to orbit <span>·</span> Scroll to zoom</div>
</article>
<div class="secondary-plots"><article class="plot small-plot"><div class="plot-heading"><div><h2>Tone map</h2><p>Frequency leaves a fingerprint</p></div><span class="mini-badge">Hz</span></div><div id="tone-canvas" class="canvas-host" aria-label="3D tone map"></div><div class="plot-footer"><span>Frequency × amplitude × time</span><span class="spectrum-line"></span></div></article><article class="plot small-plot"><div class="plot-heading"><div><h2>Modulation space</h2><p>The movement between moments</p></div><span class="mini-badge">Δ</span></div><div id="mod-canvas" class="canvas-host" aria-label="3D modulation map"></div><div class="plot-footer"><span>Frequency change × level change × time</span></div></article></div>
</div>
<div class="viewbar"><div class="view-tabs" role="group" aria-label="Main visualization"><button data-mode="constellation" class="active" aria-pressed="true">Constellation</button><button data-mode="spread" aria-pressed="false">Texture</button><button data-mode="tone" aria-pressed="false">Frequency</button><button data-mode="mod" aria-pressed="false">Modulation</button></div><span class="view-note">Explore the structure inside your sound.</span></div>
<div class="metrics"><div><span>Amplitude</span><strong id="metric-rms">—<small> dB</small></strong></div><div><span>Centroid</span><strong id="metric-centroid">—<small> Hz</small></strong></div><div><span>Spread</span><strong id="metric-spread">—<small> Hz</small></strong></div><div><span>Entropy</span><strong id="metric-entropy">—</strong></div><canvas id="spectrum" aria-label="Live frequency spectrum"></canvas></div>
<div class="player"><button id="play" class="play" aria-label="Play demo">▶</button><div class="track"><strong id="track-name">Blackbird song</strong><span id="track-detail">Diana Tudor · field recording · press play</span></div><div class="timeline"><span id="current-time">0:00</span><input id="seek" aria-label="Seek" type="range" min="0" max="70" step="0.1" value="0" disabled><span id="duration">1:10</span></div><div class="volume"><button id="mute" aria-label="Mute">♪</button><input id="volume" type="range" aria-label="Volume" min="0" max="1" step="0.01" value="0.65"></div></div>
<p id="message" role="status" aria-live="polite"></p>
</section>
</main>
<footer class="page-footer"><span>A living study of sound.</span><a href="https://www.lucioarese.net/seeing-birdsong/" target="_blank" rel="noreferrer">Inspired by Lucio Arese’s Seeing Birdsong ↗</a></footer>
</div>
<div id="drop-overlay" hidden><div>Drop your sound here<span>Let’s see what it looks like.</span></div></div>
<dialog id="about"><button id="close-help" aria-label="Close" class="quiet">×</button><h2>Sound, translated into space.</h2><p>Each point is a moment in your audio. Its position comes from the sound’s measured frequency content and amplitude. Older points form a trail through time.</p><p><strong>Constellation</strong> places detected tones into an expressive 3D shape using frequency, amplitude and texture. Floating labels show peak amplitude and frequency. Brighter lines connect successive sound samples, following similar tones where possible; softer lines connect nearby points. These are visual links, not proof of an acoustic relationship. Points stay in place and fade smoothly to zero over the trail length (six seconds by default). The final notes fade out after playback ends. The camera settles around the shape and gently widens as needed until you drag or zoom; Reset camera restores automatic framing.</p><p><strong>Texture</strong> maps spectral entropy (how dispersed the energy is) and spectral spread (frequency width). <strong>Frequency</strong> shows the strongest tones. <strong>Modulation</strong> shows frame-to-frame changes in frequency centroid and amplitude.</p><p>Drag any plot to orbit. Scroll or pinch to zoom. Sensitivity magnifies the display; it does not change your audio. Measurements use the original signal before the volume control.</p><p>This is an interpretation of the reference, not the artist’s original algorithm. The initial preview is illustrative; pressing play switches to real audio analysis. The default demo is a real blackbird field recording. It streams from Wikimedia Commons and needs an internet connection.</p><p>Birdsong: <a href="https://commons.wikimedia.org/wiki/File:Common_Blackbird_song_(Turdus_merula).ogg" target="_blank" rel="noreferrer">Common Blackbird song (Turdus merula)</a> by Diana Tudor, used unmodified under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer">CC BY 4.0</a>.</p><p>MP3, WAV, M4A, FLAC and MP4 playback depends on your browser’s codecs. Protected streaming songs cannot be opened as local files.</p></dialog>
<script type="module" src="/src/main.js"></script>
</body></html>