Spicy Player is an offline music player for Android with a port/recreation of Spicy Lyrics - A Spicetify Extension, designed to achieve visual parity with Spicy Lyrics' rendering. Built using Jetpack Compose (Canvas API) and ExoPlayer.
This repo also includes an iOS SwiftUI app target under ios/project.yml. The iOS version uses an app-local library workflow: import songs or whole folders, copy audio and .ttml files into the app's storage, auto-pair by basename, and render synchronized lyrics during playback.
Warning
This is a work in progress. The app is not yet complete and may have bugs.
- Sub-Pixel Text Positioning: Uses Compose's
TextMeasurerfor exact glyph calculation. - Duet-Aware Layout: Identifies primary (
v1) and guest (v2) artists from TTML metadata. - Intelligent Alignment: Primary artists are justified to the left, and guest artists to the right, with multi-line blocks correctly right-aligned for balance.
- Analytic Spring Engine: Replaces traditional Euler/Verlet integration with a mathematically exact (closed-form) solution for damped harmonic oscillators. This ensures that a target set 500ms in the future is reached exactly, with zero energy drift.
- Critical Damping (ζ = 1.0): Used for all auto-scroll centering to provide the fastest non-oscillatory return possible.
- Under-damped Springs: Used for word-bouncing and interlude-dot expansions to provide a lively, bouncy character.
- Held Word Bounce: Syllables with a duration
>= 1000msautomatically receive a bouncy scale and Y-offset animation, highlighting them letter-by-letter. - Instrumental Break Dots: Injected automatically for gaps
>= 3000ms, featuring a "breathe" pulse effect synced with the song's timing. - Seamless Seek: Clicking any lyric line instantly re-bases the physics spring to the current visual position, ensuring no "snap-back" jumps when transitioning from manual scrolling back to auto-focus.
The app uses a single-pass rendering loop that updates at the device's native refresh rate (60Hz/90Hz/120Hz).
- Coordinate Systems:
Canvas Space: (0, 0) is the top-left of the view.Scroll Space: A virtual Y-coordinate where the lyrics live.Screen Center: Used as the anchor point for the active line focus.
- Calculations:
targetY = -clusterCenterY(where the cluster is the average Y-position of all currently active lines).
Solving
-
Critically Damped (
$\zeta = 1$ ):$x(t) = (x_0 + (v_0 + \omega x_0)t)e^{-\omega t}$ -
Under-damped (
$\zeta < 1$ ):$x(t) = e^{-\zeta\omega t}(A \cos(\omega_d t) + B \sin(\omega_d t))$ This ensures perfect smoothness regardless of fluctuating frame times.
A stateful XML parser that:
- Scans for
<ttm:agent>metadata to identify primary artists. - Tokenizes
<p>tags into high-precisionWordobjects. - Injects virtual
Lineobjects for instrumental breaks.
Find the full feature and bug roadmap here.
- Jetpack Compose: For the entire UI declaration and Canvas manipulation.
- Media3 (ExoPlayer): Industrial-grade media decoding and playback.
- Kotlin Coroutines: For non-blocking IO during TTML and audio file scanning.
- Custom XML Pull Parser: For lightweight, low-memory performance on large lyric files.
- SwiftUI + AVFoundation: For the iOS player, local file importing, and synchronized lyric rendering.
GitHub Actions includes a manual workflow at .github/workflows/build-mobile.yml.
platform: buildandroid,ios, orbothandroid_variant: buildDebugorReleaseios_export: export an unsigned deviceipafor sideload tools or an unsignedsimulator-app
The workflow generates the Xcode project from ios/project.yml using XcodeGen on the macOS runner, then builds either:
- an unsigned device
.ipasuitable for local resigning/sideloading tools such as Sideloadly - an unsigned iOS Simulator
.appzip
The iOS app icons are generated during the Xcode build and in CI from the existing Android source logo at app/src/main/res/drawable/logo.png, so the repo does not need committed per-size iOS icon PNGs.
Import Song: copies one audio file into the app-local library and attempts to auto-import a sibling.ttmlImport Folder: recursively scans a selected folder for supported audio files and.ttmlfilesAttach Lyrics: manually pairs a selected.ttmlfile with the currently loaded track- Imported tracks persist across launches because they are stored in the app's
Application Supportdirectory
This project is licensed under the AGPL-3.0 License, inherited from the Spicy Lyrics project. See the LICENSE file for the full text.
Made by TX24 with the help of Antigravity's available models. Based on Spicy Lyrics - A Spicetify Extension