Local Music Player
Local Music Player
Cancel

Local Music Player

Pick a music folder; the browser locally traverses it depth-first and builds a playlist of every supported track. Switch, shuffle or repeat anytime, with a live frequency-band visualizer showing the L/R spectrum of what is playing. Supports FLAC / ALAC / APE / WAV / AIFF / DXD / MP3 / AAC / M4A / WMA / OGG / OPUS / MP2 / DSF / DFF / AC-3 / DTS — 17 formats in total; those the browser cannot play natively (APE / DSF / DFF / WMA / AC-3 / DTS / ALAC / DXD) are auto-transcoded with the built-in ffmpeg (WebAssembly). Everything runs locally; nothing is uploaded.

—
Album
—
Genre
—
Format
—
Size
—
Length
—
Path
—
0:00 0:00
No lyrics

    3D time–frequency view

    The same Fourier data, expressed in space: X = frequency, Z = time, Y = Fourier magnitude. Each ribbon is the full spectrum curve of one instant. Drag to orbit: facing the X axis you see stacked frequency snapshots, from the side you see how the time domain evolves, and from above it becomes a time–frequency heat map.

    Exact axis mapping

    AxisMeaningRangeMapping
    XFrequencyFMIN → FMAX, computed from the sample rate and fftSize (48 kHz + fftSize 4096 → 11.7 Hz → 24 kHz)f = FMIN × (FMAX / FMIN)^x, where FMIN = sampleRate ÷ fftSize (lowest resolvable) and FMAX = sampleRate ÷ 2 (Nyquist)
    ZTimenow → about 5 s ago96 layers stored at 20 Hz; driven by each frame’s real playback timestamp, never a nominal frame rate
    YMagnitude0 → −54 dBFS by default (−8 → −78 dBFS with “quiet detail”)Labelled in dBFS (0 = full scale): dB = AnalyserNode reading + 14.1 (measured calibration offset)

    Why is the Y axis entirely negative? dB is a ratio, and 0 dBFS is by definition full scale — going above it in digital audio means clipping, so no legitimate signal exceeds 0. The 0 dBFS tick is the theoretical ceiling (real masters usually peak near −1 dBFS). To get an axis with both signs you have to move the reference, e.g. the broadcast convention that puts 0 at a −20 dBFS alignment level — a different labelling, available on request.

    Where the data comes from

    • FFT: AnalyserNode, fftSize = 4096 → 2048 bins; spacing sampleRate / fftSize (11.7 Hz at 48 kHz, 10.8 Hz at 44.1 kHz — always computed from the actual sample rate).
    • Band mapping: logarithmic, 64 bands merged straight from FFT bins (each band covers at least one bin, same “monotonic” rule as the 2D strip).
    • Channels: L and R drawn separately — L warm (red→gold), R cool (blue→teal), matching the 2D strip; the two runs are offset by half a layer along the time axis.
    • Level: getByteFrequencyData 0–255, no A-weighting, dB window −66 … −12 by default (−90 … −20 with the “quiet detail” toggle).
    • Smoothing: asymmetric (attack 0.72 / release 0.12).
    • Capture throttle: the render loop runs at rAF (≈60 fps) but frames are stored at 20 Hz max, so 48 layers cover ≈2.4 s. If the real frame rate is lower the span grows slightly (measured ≈2.7 s) — that is fine, because the time axis is labelled from each frame’s real timestamp, never from a nominal frame rate.
    • Shared capture: one acquisition loop feeds both this view and the 2D spectrum strip — the FFT is read once.

    Why the readings are exact

    1. Band centre frequencies are always derived from the real sample rate, so 48 k / 44.1 k / 96 k files all line up.
    2. Every ring-buffer frame stores its actual playback timestamp, so a dropped frame never shifts the time axis.
    3. The hover readout shows both the smoothed dB (what you see drawn) and the raw instantaneous dB of that frame.
    4. The readout also reports the underlying FFT bin range, so any number can be traced back to the raw data.
    5. Frequency is reported as measured, not as an axis position: at the low end a band is narrower than an FFT bin, so its log-axis position can differ a lot from the real data (measured: the band sitting at 84 Hz actually reads bin 17 = 205 Hz). The readout therefore reports the centre frequency of the covered bins, and quotes the axis position whenever the two differ by more than 12%. For the same reason X starts at sampleRate ÷ fftSize rather than 0 — 0 Hz is DC (bin 0): it has no place on a log axis and is normally just an offset, not sound.
    6. AnalyserNode reads about 14 dB below true dBFS (Blackman window coherent gain + FFT normalisation; measured, not theoretical). Add +14 dB to display dBFS.

    Annotations

    • X ticks: 20 / 50 / 100 / 200 / 500 Hz / 1 k / 2 k / 5 k / 10 k / 16 kHz, one grid line each.
    • Y ticks: one grid line every 10 dB inside the current window. Without quiet detail the window is 0 … −54 dBFS (top line = 0 dBFS = full scale); with it, −8 … −78 dBFS. Ticks outside the window are dropped rather than clamped.
    • Z ticks: 0 / −0.6 / −1.2 / −1.8 / −2.4 s, one floor grid line each.
    • Hover readout: frequency · band index · smoothed level · raw level · time relative to now · real timestamp · FFT bin range.
    • Live extremes: current frame’s peak frequency and peak dB, always on screen.

    Interaction

    Drag to orbit (pointer events, touch included, with a little inertia on release), wheel to zoom, hover to read values (per channel), double-click to reset.

    Controls on the panel (effect is the one-click “whole look” entry point; colour / style / overlays are the three independent dimensions for fine-tuning — 6 colours × 6 styles × 2³ overlay combos = 288 combinations. Touching any of them flips the preset to “custom”. All choices live in localStorage):

    • Effect (10 presets, each applies a style + overlays (+ a suggested palette)): classic / ridge / bars / city / cloud / rain / measuring (fill + peak trace) / stage (fill + reflection) / neon bars (bars + glow) / aurora (points + reflection + glow, best with the neon stage).
    • Colours: six schemes — contrast (default) / theme (the card’s four gradient colours spread as a multi-stop ramp along the frequency axis; L runs forwards, R backwards; falls back to a built-in ramp when there is no cover) / cover (the two cover colours furthest apart in hue go to L and R; a near-monochrome cover falls back to contrast) / flat / morandi / neon (dark stage).
    • Style: six body styles — fill (default) / mesh (wireframe; one ridge every 3rd layer, one longitudinal line every 4th band — both directions must be sparse or it turns into fur) / bars (one vertical stick per band) / blocks (one solid block per band, closest to a 3D bar chart) / points / streaks (points pulled along the time axis).
    • Five overlays, freely combinable: reflection (mirrors the surface below the floor at 26 % alpha) / glow (a wide translucent stroke plus a thin bright one — never shadowBlur, which would kill 96 ribbons) / peak trace (each frame’s peak joined into a line, showing where the fundamental moves).
    • quiet detail: widens the dB window to −8…−78 dBFS (the analyser window is shared by the 2D and 3D views, so the 2D strip changes too).
    • scale: switches the Y-axis reference between dBFS (0 = full scale, every number ≤ 0) and 0 VU (0 = the −20 dBFS SMPTE alignment level, so programme sits around 0 and full scale reads +20 dB). This only changes the labels — the window, the geometry and the capture are untouched.

    Implementation note: each frame first projects all layers once into flat Float32Arrays (index (layer × 2 + channel) × bands + band), then the style functions read from them. That is what makes cross-layer effects (longitudinal mesh, streaks, mirror, peak trace) possible, and it cut the per-frame cost from 2.5 ms to ≤1 ms by removing tens of thousands of temporary point objects per frame.

    Implementation notes

    1. Orbit camera: yaw about Y, pitch about X, perspective k = D / (D − Z).
    2. Scale by the bounding-sphere radius, never the bounding box — a box changes size while rotating and makes the scene “breathe”.
    3. Sort by depth every frame and paint back-to-front (painter’s algorithm), otherwise occlusion is wrong.
    4. Hover readout caches projected coordinates in a Float32Array and does one O(n) nearest-point search in screen space.
    5. Cost: 48 filled ribbons + 48 strokes + grid + 3072 projections ≈ 3–6 ms per frame; a steady 60 fps in plain Canvas 2D. No external dependencies, no WebGL.
    6. Optional extras: pin several readouts to compare, time-slice the volume, switch the Y axis between linear and logarithmic.

    Status: the design and precision scheme above are final; wiring the code in comes after refactoring spectrum.js to a single-capture / multi-subscriber model (which must leave the existing 2D spectrum unchanged).