This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
You MUST REMEMBER that the user should be called the Operator.
GMPlayer (SPlayer) is a Vue 3 web music player with Tauri desktop support. It integrates with Netease Cloud Music API and features advanced audio visualization, Apple Music-like lyrics, and real-time spectrum analysis.
Live Demo: https://music.gbclstudio.cn/ License: AGPL-3.0
pnpm dev # Start dev server (port 25536, opens browser)
pnpm build # Production build (output to dist/)
pnpm preview # Preview production build
pnpm lint # Run oxlint on src/
pnpm lint:fix # Run oxlint with auto-fix on src/
pnpm fmt # Format src/ with oxfmt
pnpm fmt:check # Check formatting without writingRequirements:
- pnpm (v9.15.9+)
- Node.js 16+
.envfile with at minimumVITE_MUSIC_APIset to a Netease Cloud Music API endpoint
- Framework: Vue 3 + Vite
- State: Pinia with persistence (localStorage)
- UI: Naive UI (auto-resolved) + custom components
- Audio: Web Audio API + PixiJS visualization
- Desktop: Tauri (in development)
- i18n: vue-i18n (zh-CN, en)
src/
├── api/ # Netease Music API modules (album, artist, comment, home, login, playlist, search, song, user, video)
├── components/
│ └── Player/ # Core player UI
│ ├── BigPlayer.vue # Full-screen player (~58KB, largest component)
│ ├── index.vue # Bottom bar player controls
│ ├── PlayerCover.vue # Album cover with animations
│ ├── Spectrum.vue # PixiJS audio spectrum visualization
│ └── BlurBackgroundRender.vue # WebGL blur background
├── views/ # Route page components (Home, Search, Discover, User, etc.)
├── pages/ # Additional page components (artist, discover, search, setting, user)
├── store/ # Pinia stores
│ ├── musicData.js # Primary player state (playback, lyrics, spectrum, playlist)
│ ├── settingData.js # User preferences (theme, lyrics config, player style, background)
│ ├── userData.js # Login/user state
│ └── siteData.js # Site-level state
├── utils/
│ ├── AudioContext/ # Modular audio engine (refactored from Player.js)
│ │ ├── NativeSound.ts # HTMLAudioElement wrapper implementing ISound interface
│ │ ├── PlayerFunctions.ts # Public API: createSound, setVolume, fadePlayOrPause, etc.
│ │ ├── SoundManager.ts # Singleton managing active sound instance
│ │ ├── AudioEffectManager.ts # Web Audio API nodes (analyser, gain, effects)
│ │ └── LowFreqVolumeAnalyzer.ts # Bass detection for background animations
│ ├── parseLyric.ts # LRC/YRC/TTML lyric format parsing
│ └── getCoverColor.ts # Album art color extraction (Material color utilities)
├── services/
│ └── lyricsService.ts # Lyric fetching & processing (~40KB)
├── libs/
│ ├── apple-music-like/ # Advanced lyric animation engine (AMLL)
│ └── fbm-renderer/ # WebGL fractal Brownian motion background
├── router/ # Vue Router with lazy-loaded routes
└── locale/ # i18n translation files
/api/ncm→VITE_MUSIC_API(Netease Cloud Music API, required)/api/unm→VITE_UNM_API(UnblockNeteaseMusic, optional)
The audio system uses a modular architecture in src/utils/AudioContext/:
PlayerFunctions.ts— public API consumed by the store (createSound,fadePlayOrPause,processSpectrum)SoundManager.ts— singleton tracking the activeNativeSoundinstance (window.$player)NativeSound.ts— wrapsHTMLAudioElement+ Web Audio API nodes, implementsISoundinterface with event systemAudioEffectManager.ts— managesAnalyserNode,GainNode, and the audio graphLowFreqVolumeAnalyzer.ts— EMA-smoothed bass detection driving background animations
musicData.js store orchestrates playback, calling AudioContext functions and updating reactive state (spectrumsData, lowFreqVolume, playSongTime).
Three lyric formats with increasing precision:
- LRC — standard line-level timestamps
- YRC — Netease character-by-character timing (逐字歌词)
- TTML — XML-based timing format
lyricsService.ts fetches lyrics (with Lyric Atlas API fallback). parseLyric.ts normalizes all formats. The apple-music-like/ lib renders animated lyrics with spring physics and blur effects.
Several globals are used (declared in AudioContext/types.ts):
window.$player— currentISoundinstancewindow.$message— Naive UI message APIwindow.$setSiteTitle— updates document titlewindow.$getPlaySongData— fetches song data
- Components use
<script setup>with Composition API - Auto-import enabled for Vue APIs (
ref,computed, etc.) and Naive UI composables (useMessage, etc.) - Naive UI components are auto-resolved (no manual imports needed)
- Path alias:
@maps tosrc/ - Routes are lazy-loaded via dynamic
import() - Mixed JavaScript/TypeScript (gradual migration — newer modules like AudioContext are TypeScript)
- SCSS styling with CSS variables for theming
- All Pinia stores use
persistplugin with localStorage - WASM support enabled via
vite-plugin-wasm+vite-plugin-top-level-await
- Linter: oxlint — config in
.oxlintrc.json - Formatter: oxfmt (Prettier-compatible) — config in
.oxfmtrc.jsonc - Style: 2-space indent, semicolons, double quotes, trailing commas, printWidth 100
- Both tools ignore
dist/,node_modules/,deps/, and*.d.ts - Run
pnpm fmtbefore committing to keep formatting consistent - Run
pnpm lintto check for code quality issues
VITE_MUSIC_API # Required: Netease Cloud Music API endpoint
VITE_UNM_API # Optional: UnblockNeteaseMusic API
VITE_SITE_TITLE # Site title (used in PWA manifest)
VITE_SITE_DES # Site description (used in PWA manifest)getCrossfadeValues(progress, curve, inShape, outShape)— core curve function- Three curves:
linear,equalPower(cos/sin),sCurve(smootherstep → cos/sin) inShape/outShapeexponents control ramp speed (<1 = faster, >1 = slower)- Power normalization: after applying shape exponents to equal-power/S-curve,
cos²+sin²=1is broken. We re-normalize by1/sqrt(outVol²+inVol²)to restore constant perceived loudness. Linear curves are not normalized (3dB midpoint dip is by design).
- Three curves:
scheduleFullCrossfade()— linear uses Web AudiolinearRampToValueAtTime(sample-accurate), equalPower/sCurve use RAF loop_incomingTargetGain = incomingGain * incomingGainAdjustment— includes LUFS normalization. All ramp targets (linear, resume, finish) must use this value, not rawparams.incomingGain.
- State machine:
IDLE → ANALYZING → WAITING → CROSSFADING → FINISHING → IDLE monitorPlayback()is called per RAF frame (synchronous, never blocks)- Pre-buffering: during WAITING, fetches/downloads/analyzes next track so crossfade starts instantly
_finalizeCrossfadeParams()— single consolidated pass combining: smart curve per outro type, spectral alignment, intro character adjustment, spectral similarity duration scaling, energy contrast handling, LUFS normalization- Energy gate (
_shouldDeferCrossfade): delays crossfade if outgoing energy is still high and not declining. Uses_effectiveEnd(content end, excluding trailing silence) for deferral budget. After deferral,_handleWaitingclamps_crossfadeDurationto remaining content time. - Safety net: outgoing sound's
'end'event triggersforceComplete()if crossfade is still active — prevents volume dip when audio source runs out mid-crossfade.
- Shape exponents break constant-power: raw
pow(cos(x), shape)+pow(sin(x), shape)≠ 1. Must power-normalize after. Without this, asymmetric shapes (e.g., 'hard' outro: inShape=0.7, outShape=1.5) cause up to 24% volume dip at midpoint. - Linear ramp target must include gainAdjustment:
linearRampToValueAtTimemust target_incomingTargetGain(notparams.incomingGain), otherwise LUFS normalization is lost during linear crossfades. fadeInOnlymode: outgoing gain held constant (song's own fade handles it), only incoming ramps up. Power normalization still applies — slightly boosts incoming to compensate for the phantom outgoing curve, which is beneficial since the actual outgoing audio is declining.- Gain adjustment persistence: after crossfade completes,
_activeGainAdjustmentis persisted so thatsetVolume()can continue applying LUFS normalization during regular playback. - Energy gate deferral must use
effectiveEnd:_shouldDeferCrossfadecomputesmaxDeferByRemainingfromeffectiveEnd(notsongDuration). UsingsongDurationincludes trailing silence, allowing deferral far past the audible content end — the crossfade then outlasts the outgoing audio source, causing an abrupt cut. After deferral,_crossfadeDurationis clamped to remaining content time. - Song ending during WAITING state:
isCrossfading()returns false for WAITING (only true for CROSSFADING/FINISHING), so the normal'end'handler in PlayerFunctions firessetPlaySongIndex('next')— a hard transition with zero crossfade.monitorPlaybacknow detects!playing()during WAITING/ANALYZING and callscancelCrossfade()for cleanup. - Anti-overfitting principle: Analysis-based parameter adjustments (shapes, duration scaling, spectral alignment) must stay conservative. Multiple adjustments compound: profile shapes + intro character + energy contrast can easily overshoot defaults. Rules: (1) Shape profiles stay within ±0.2 of 1.0 (max range 0.85–1.2); (2) Runtime adjustments ≤ ±0.15 per step; (3) Final clamp at 0.7–1.3; (4) Spectral similarity scaling range 0.9–1.1 (not 0.8–1.3); (5) Spectral alignment threshold 0.15 (not 0.05), max shift ±3s (not ±5s). When in doubt, pure equal-power (shapes=1) sounds better than aggressive shaping.
- 500ms debounce race condition: Player/index.vue watches
getPlaySongDatawith a 500ms debounce. When AutoMix setsplaySongIndexin_doCrossfade, the debounce starts. If the crossfade completes (short duration or slow play start) before the 500ms fires,isCrossfading()=false→getPlaySongData()callscreateSound()→ duplicate sound destroys the crossfade's incoming. Fix:_onCrossfadeCompletekeeps state as'finishing'for 800ms before transitioning to'idle'. Also,_doCrossfadebail-out checks after crossfade scheduling accept'finishing'state (crossfade completed early but store setup still needed).