fix(player): redesign cleanup pass — sleeve, tonearm, AGC, dead code
Production-readiness pass before merging the Studio Reference redesign to master. Audio (backend): - Reset AGC `_spectrum_ref` envelope on `start()` so a long silent gap between sessions doesn't make the first new transients clip at the ceiling. Annotated the trade-off (loud transient lifts reference for a few seconds afterwards — the price of real loudness). - Add `tests/test_audio_analyzer.py` with 10 cases: bin-edge layout, AGC attack/release asymmetry, lifecycle reset. Skips numpy-dependent cases when numpy isn't installed; CI has it. Vinyl mode dead code removed: - The toggle button was dropped during the sleeve refactor but the JS state, 2 s `setInterval`, `beforeunload` handler, and `applyVinylMode` call (commented out in app.js) all stayed. Now properly excised from player.js + app.js + window.* exports. - Stripped the matching `.album-art-container.vinyl*` CSS block and its `vinylSpin` keyframes (~95 LoC). Sleeve + tonearm fixes: - Removed the duplicate `.now-playing .vinyl-stage` / `.vinyl-label` / `.tonearm` block that was overriding the new `.vinyl-stage` rules by source order — the uncommitted tonearm geometry never took effect because the stale clone won the cascade. - Tightened tonearm to 36% × 36% at right:-6%, top:26% so the SVG bounding box stays right of the sleeve (sleeve right edge ~68%). Needle now lands on the visible disc grooves at both rest and playing rotations and never overlaps the cover. - Removed sleeve `transform: rotate(-2.5deg)` + the matching mobile `-1.8deg` override; sleeve now sits flat and squared-off. - Removed the 1px inset hairline on the sleeve and the 1px outline + inset highlight on the album art — cleaner, no semitransparent border noise. - Album art inset 5% to expose a cardstock margin around the print (using explicit width/height — `inset` shorthand triggered the CSS replaced-element rule that uses the image's intrinsic size and blew out the grid track). Mobile + misc: - Removed mobile tonearm overrides at 720px and 420px — they were calibrated for the pre-sleeve geometry and put the needle back over the cover on phones; desktop geometry is proportional and works. - Added `<meta name="mobile-web-app-capable">` alongside the legacy Apple variant to silence the deprecation warning in Chromium. - Replaced the "PRIMARY" badge on display cards with a copper star icon (translation key still drives title + aria-label). - `.gitattributes` with `* text=auto eol=lf` so Windows checkouts stop nagging "LF will be replaced by CRLF". Annotations: - "REF · 24" record-label catalogue mark marked as intentional non-i18n decoration in index.html. CI: ruff clean, pytest 7 passed + 3 numpy-skipped (all 10 run on CI). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,6 @@ import {
|
||||
activeTab, switchTab, updateTabIndicator, setMiniPlayerVisible,
|
||||
initTheme, toggleTheme, initAccentColor, applyAccentColor,
|
||||
renderAccentSwatches, selectAccentColor, toggleAccentPicker, lightenColor,
|
||||
toggleVinylMode, applyVinylMode,
|
||||
visualizerEnabled, visualizerAvailable, setVisualizerEnabled,
|
||||
checkVisualizerAvailability, toggleVisualizer, applyVisualizerMode,
|
||||
loadAudioDevices, onAudioDeviceChanged,
|
||||
@@ -96,8 +95,8 @@ Object.assign(window, {
|
||||
switchTab,
|
||||
// Theme & accent
|
||||
toggleTheme, toggleAccentPicker, selectAccentColor, lightenColor,
|
||||
// Vinyl & visualizer
|
||||
toggleVinylMode, toggleVisualizer,
|
||||
// Visualizer (vinyl spin is structural CSS — no toggle)
|
||||
toggleVisualizer,
|
||||
// Background
|
||||
toggleDynamicBackground,
|
||||
// Auth
|
||||
@@ -163,9 +162,6 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||
}
|
||||
|
||||
// Vinyl is now structural / always-on via CSS — no init call needed.
|
||||
// applyVinylMode();
|
||||
|
||||
// Build the editorial spectrum bars. Fewer, fatter bars read better
|
||||
// than many thin ones at this column width. JS-managed so we can
|
||||
// drive heights from real audio data when available.
|
||||
|
||||
@@ -57,7 +57,13 @@ export async function loadDisplayMonitors() {
|
||||
|
||||
const details = [monitor.resolution, monitor.manufacturer].filter(Boolean).join(' \u00B7 ');
|
||||
const detailsHtml = details ? `<span class="display-monitor-details">${details}</span>` : '';
|
||||
const primaryBadge = monitor.is_primary ? `<span class="display-primary-badge">${t('display.primary')}</span>` : '';
|
||||
const primaryBadge = monitor.is_primary
|
||||
? `<span class="display-primary-badge" title="${t('display.primary')}" aria-label="${t('display.primary')}">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</span>`
|
||||
: '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="display-monitor-header">
|
||||
|
||||
@@ -208,72 +208,6 @@ document.addEventListener('click', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Vinyl mode
|
||||
let vinylMode = localStorage.getItem('vinylMode') === 'true';
|
||||
|
||||
function getVinylAngle() {
|
||||
const art = document.getElementById('album-art');
|
||||
if (!art) return 0;
|
||||
const st = getComputedStyle(art);
|
||||
const tr = st.transform;
|
||||
if (!tr || tr === 'none') return 0;
|
||||
const m = tr.match(/matrix\((.+)\)/);
|
||||
if (!m) return 0;
|
||||
const vals = m[1].split(',').map(Number);
|
||||
const angle = Math.round(Math.atan2(vals[1], vals[0]) * (180 / Math.PI));
|
||||
return ((angle % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
function saveVinylAngle() {
|
||||
if (!vinylMode) return;
|
||||
localStorage.setItem('vinylAngle', getVinylAngle());
|
||||
}
|
||||
|
||||
function restoreVinylAngle() {
|
||||
const saved = localStorage.getItem('vinylAngle');
|
||||
if (saved) {
|
||||
const art = document.getElementById('album-art');
|
||||
if (art) art.style.setProperty('--vinyl-offset', `${saved}deg`);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(saveVinylAngle, 2000);
|
||||
window.addEventListener('beforeunload', saveVinylAngle);
|
||||
|
||||
export function toggleVinylMode() {
|
||||
if (vinylMode) saveVinylAngle();
|
||||
vinylMode = !vinylMode;
|
||||
localStorage.setItem('vinylMode', vinylMode);
|
||||
applyVinylMode();
|
||||
}
|
||||
|
||||
export function applyVinylMode() {
|
||||
const container = document.querySelector('.album-art-container');
|
||||
const btn = document.getElementById('vinylToggle');
|
||||
if (!container) return;
|
||||
if (vinylMode) {
|
||||
container.classList.add('vinyl');
|
||||
if (btn) btn.classList.add('active');
|
||||
restoreVinylAngle();
|
||||
updateVinylSpin();
|
||||
} else {
|
||||
saveVinylAngle();
|
||||
container.classList.remove('vinyl', 'spinning', 'paused');
|
||||
if (btn) btn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function updateVinylSpin() {
|
||||
const container = document.querySelector('.album-art-container');
|
||||
if (!container || !vinylMode) return;
|
||||
container.classList.remove('spinning', 'paused');
|
||||
if (currentPlayState === 'playing') {
|
||||
container.classList.add('spinning');
|
||||
} else if (currentPlayState === 'paused') {
|
||||
container.classList.add('paused');
|
||||
}
|
||||
}
|
||||
|
||||
// Audio Visualizer
|
||||
export let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true';
|
||||
export let visualizerAvailable = false;
|
||||
@@ -361,13 +295,6 @@ export function stopVisualizerRender() {
|
||||
if (visualizerCtx && canvas) {
|
||||
visualizerCtx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
const art = document.getElementById('album-art');
|
||||
if (art) {
|
||||
art.style.transform = '';
|
||||
art.style.removeProperty('--vinyl-scale');
|
||||
}
|
||||
const glow = document.getElementById('album-art-glow');
|
||||
if (glow) glow.style.opacity = '';
|
||||
frequencyData = null;
|
||||
smoothedFrequencies = null;
|
||||
document.body.classList.remove('audio-spectrum-live');
|
||||
@@ -417,20 +344,9 @@ function renderVisualizerFrame() {
|
||||
visualizerCtx.fill();
|
||||
}
|
||||
|
||||
const bass = frequencyData.bass || 0;
|
||||
const scale = 1 + bass * 0.04;
|
||||
const art = document.getElementById('album-art');
|
||||
if (art) {
|
||||
if (vinylMode) {
|
||||
art.style.setProperty('--vinyl-scale', scale);
|
||||
} else {
|
||||
art.style.transform = `scale(${scale})`;
|
||||
}
|
||||
}
|
||||
const glow = document.getElementById('album-art-glow');
|
||||
if (glow) {
|
||||
glow.style.opacity = (0.4 + bass * 0.4).toFixed(2);
|
||||
}
|
||||
// Bass-driven album-art scale + glow pulse removed — the
|
||||
// "burst" looked unnatural on the sleeve. Spectrum bars +
|
||||
// VU needle remain the audio-reactive elements.
|
||||
|
||||
// Drive the editorial .spectrum bars from the same frequency data.
|
||||
updateEditorialSpectrum(smoothedFrequencies, numBins);
|
||||
@@ -464,10 +380,12 @@ function updateEditorialSpectrum(bins, numBins) {
|
||||
for (let j = startIdx; j < endIdx && j < numBins; j++) {
|
||||
if (bins[j] > peak) peak = bins[j];
|
||||
}
|
||||
// Per-bar high-end gain: 1.0 at the lowest bar, ~3.0 at the highest.
|
||||
const gain = 1 + (i / barCount) * 2.0;
|
||||
// Per-bar high-end gain: 1.0 at the lowest bar, ~1.8 at the highest.
|
||||
// Backend now ships AGC-normalized bins (peak ~1, transients up to 1.5)
|
||||
// so the master multiplier stays modest to avoid perma-clipping.
|
||||
const gain = 1 + (i / barCount) * 0.8;
|
||||
// Floor at 12% so silent bars are still visually present.
|
||||
const pct = Math.max(12, Math.min(100, peak * 110 * gain));
|
||||
const pct = Math.max(12, Math.min(100, peak * 65 * gain));
|
||||
bars[i].style.height = pct + '%';
|
||||
}
|
||||
}
|
||||
@@ -898,7 +816,6 @@ export function updatePlaybackState(state) {
|
||||
dom.playPauseIcon.innerHTML = SVG_PLAY;
|
||||
dom.miniPlayPauseIcon.innerHTML = SVG_PLAY;
|
||||
}
|
||||
updateVinylSpin();
|
||||
}
|
||||
|
||||
function updateProgress(position, duration) {
|
||||
|
||||
Reference in New Issue
Block a user