updated readme
This commit is contained in:
parent
0e6f4852e5
commit
228b186569
1 changed files with 233 additions and 168 deletions
385
README.md
385
README.md
|
|
@ -1,118 +1,247 @@
|
|||
# NavidromePlayer
|
||||
|
||||
A native iOS + watchOS music player for [Navidrome](https://www.navidrome.org/) servers, built with SwiftUI and AVFoundation. Features a Mitsuha-style audio visualizer, Apple Watch companion app, radio streaming with Shazam identification, and an optional Companion API for advanced server-side features.
|
||||
> *Your music library. Your hardware. No subscriptions. No ads. No cloud.*
|
||||
|
||||
A native iOS + watchOS music player handcrafted in Swift for [Navidrome](https://www.navidrome.org/) servers. Built because every streaming app charges rent for music you already own — and none of them have a waveform that looks like this.
|
||||
|
||||
---
|
||||
|
||||
## The Visualizer
|
||||
|
||||
This is the part worth talking about first.
|
||||
|
||||
NavidromePlayer renders a **Mitsuha-style liquid waveform** — named after the classic anime visualization that made smooth, flowing audio waves famous. Every frame is drawn with Catmull-Rom splines, a curve interpolation algorithm that eliminates the jagged polygon edges of typical FFT displays and replaces them with fluid, continuous shapes that genuinely move with the music.
|
||||
|
||||
### Three-source pipeline
|
||||
|
||||
The visualizer automatically selects the best available data for the current track:
|
||||
|
||||
**1. Server-precomputed frames** *(best — requires Companion API)*
|
||||
The Companion API runs a full 1024-point FFT pass over your audio files ahead of time, storing the results as compact frame arrays. When you play a song, the app downloads these frames once and caches them locally. Playback syncs the frame index to `currentTime / duration` — frame-perfect visualization with zero CPU cost during playback. A 4-minute song generates ~7,200 frames.
|
||||
|
||||
**2. On-device offline analysis** *(great — for downloaded songs)*
|
||||
For locally stored files, `OfflineAudioAnalyzer` runs a custom Swift/Accelerate FFT pipeline using `vDSP_fft` with a Hann window, a hop size calculated from your target FPS, and a ring buffer that processes the file faster than real-time. The result is identical frame-perfect sync without requiring the Companion API. Analysis runs in a background Task and cancels immediately if you exit the app — it never interferes with battery life.
|
||||
|
||||
**3. Live engine FFT** *(good — for streaming)*
|
||||
When no cached data exists, an `MTAudioProcessingTap` is installed directly on the `AVPlayerItem.audioMix`, tapping the audio stream at the hardware level. FFT results feed the visualizer at 30fps with no perceptible latency.
|
||||
|
||||
### The rendering chain
|
||||
|
||||
Each frame goes through several stages before anything appears on screen:
|
||||
|
||||
- **Log-frequency binning** — raw FFT bins are mapped logarithmically so bass frequencies get appropriate visual weight relative to treble. Linear FFT looks wrong; your ears are logarithmic.
|
||||
- **Dynamic gain normalization** — a peak follower tracks the loudest frame seen so far and scales the entire output accordingly. Quiet passages still look interesting. Loud passages don't clip.
|
||||
- **Exponential smoothing** — `displayLevel += (target - displayLevel) * smoothFactor` applied per-bar every frame. The `viscosity` setting controls how snappy or liquid the response feels.
|
||||
- **Depth shadow** — a second wave drawn behind the main wave at reduced opacity, phase-shifted using a 60/40 blend of current and next Y values to create a parallax drag effect. This gives the wave a sense of depth without any GPU compositing.
|
||||
- **Wobble phase** — an independent time-based phase offset drives a slow undulation in the wave baseline, keeping the idle state alive and giving the playing state a subtle organic quality.
|
||||
- **Catmull-Rom splines** — final bar heights are connected with Catmull-Rom interpolation rather than straight lines. The curve passes smoothly through every data point.
|
||||
|
||||
### Four styles
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
| **Wave** | Classic Mitsuha liquid waveform with depth shadow and Catmull-Rom curves |
|
||||
| **Siri Wave** | Symmetric wave mirrored above and below center |
|
||||
| **Bar** | Vertical frequency bars with rounded tops |
|
||||
| **Line** | Minimal single-line trace through the frequency data |
|
||||
|
||||
### Color modes
|
||||
|
||||
- **Dynamic** — colors shift continuously through a hue cycle at a configurable speed
|
||||
- **Album art** — dominant color extracted from the current track's cover art using a custom pixel sampler, changes on every track
|
||||
- **Custom** — a fixed color you pick, applied consistently
|
||||
|
||||
### Per-view configuration
|
||||
|
||||
Now Playing and Mini Player are configured independently. Each has its own point count (8–64), sensitivity, color mode, and style. The Mini Player visualizer suspends automatically when the full Now Playing view is open.
|
||||
|
||||
### The architecture rule that makes it work
|
||||
|
||||
The entire visualizer lives inside a single `TimelineView(.periodic)` that never unmounts while enabled. Rendering mode (live vs. idle) is decided inside the Canvas body, not by swapping views. This eliminates a SwiftUI compositor bug where the old idle Canvas persists in the render pipeline while the new live view composites its first frame — manifesting as a frozen wave after pause/resume. One stable view identity. No stale frames.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### iOS App
|
||||
- **Full Navidrome/Subsonic library** — browse by albums, artists, songs, genres, playlists
|
||||
- **Offline playback** — download songs for offline listening with per-track and per-album downloads
|
||||
- **Mitsuha visualizer** — real-time FFT waveform visualization with Catmull-Rom spline rendering, multiple styles (wave, bar, line), color modes (dynamic, album art, custom), and configurable presets
|
||||
- **Now Playing** — full-screen player with album art color extraction, drag-to-dismiss, AirPlay, seek bar, queue management
|
||||
- **Mini player** — persistent bottom bar with scrubbable progress, visualizer overlay, transport controls
|
||||
- **Radio streaming** — live radio with HLS/PLS/M3U playlist resolution, timeshift buffering, recording, and Shazam identification
|
||||
- **Smart DJ** — crossfade engine with silence skipping, loudness normalization (LUFS), and predictive transitions (requires Companion API)
|
||||
- **Batch metadata editing** — edit album/artist tags across multiple albums simultaneously (requires Companion API)
|
||||
- **Custom album covers** — long-press album art to replace with photos from your library
|
||||
- **Multi-server support** — automatic failover between servers with the same credentials
|
||||
- **Background audio** — Lock Screen / Control Center controls, Dynamic Island support
|
||||
- **Keyboard dismiss** — tap anywhere outside text fields or scroll to dismiss
|
||||
### iOS
|
||||
|
||||
### watchOS App
|
||||
- **Offline playback** — transfer songs from iPhone to Apple Watch for standalone listening
|
||||
- **Bluetooth + Speaker mode** — Apple Watch Ultra speaker support via HKWorkoutSession
|
||||
- **Crown control** — Digital Crown volume control with haptic feedback
|
||||
- **Compact visualizer** — waveform visualization on watch face
|
||||
**Library**
|
||||
- Browse albums, artists, songs, genres, playlists — all cache-first for instant loads on warm launch
|
||||
- Alphabetical all-songs list loaded via `search3` API pagination (no N+1 album fetches)
|
||||
- Long-press any song for a context menu: Play Now, Play Next, Add to Queue, Download, Send to Watch, Add to Playlist
|
||||
- Album and artist detail views with full discography
|
||||
|
||||
### Companion API (Optional)
|
||||
A Python FastAPI server that runs alongside Navidrome on your server (e.g., Raspberry Pi) providing:
|
||||
- **Smart DJ analysis** — BPM detection, silence boundary mapping, LUFS loudness measurement
|
||||
- **Mitsuha visualizer pre-computation** — generate FFT frames on the server instead of on-device
|
||||
- **Remote ID3 tag editing** — edit metadata on server files via mutagen
|
||||
- **File uploads** — upload and auto-tag new music via the app
|
||||
- **WebSocket push** — real-time notifications to the iOS app when metadata changes or uploads complete
|
||||
- **Navidrome scan trigger** — automatically rescan after tag edits
|
||||
**Playback**
|
||||
- AVPlayer for streaming, AVAudioEngine for local FFT-enabled playback
|
||||
- Smart DJ crossfade — dual AVPlayer A/B engine with configurable crossfade duration
|
||||
- Skip silence — jumps leading and trailing silence using pre-analyzed boundaries
|
||||
- LUFS loudness normalization — every song plays at the same perceived volume
|
||||
- AirPlay output selection
|
||||
- Background audio with Lock Screen / Control Center / CarPlay controls
|
||||
- Dynamic Island now playing indicator
|
||||
|
||||
**Now Playing**
|
||||
- Full-screen player with album art color extraction
|
||||
- Drag-to-dismiss gesture
|
||||
- Scrubbable seek bar with SiriKit-style scrubbing speed reduction on lateral movement
|
||||
- Queue view and management
|
||||
- Repeat and shuffle modes
|
||||
- Shazam identification — identify the currently playing track or live radio
|
||||
|
||||
**Mini Player**
|
||||
- Persistent bottom bar with scrubbable progress, visualizer overlay, transport controls
|
||||
- Tap to expand, swipe up to open Now Playing
|
||||
|
||||
**Offline**
|
||||
- Per-track and per-album downloads with inline progress indicators
|
||||
- Download All on songs list
|
||||
- `AudioPreFetcher` silently pre-caches the next 3 tracks in the foreground for instant gapless starts
|
||||
|
||||
**Radio**
|
||||
- Live streaming with HLS/PLS/M3U/ASX playlist resolution
|
||||
- Timeshift buffer — scrub back through recent live audio
|
||||
- Recording — capture radio segments to local files
|
||||
- Shazam identification for live streams
|
||||
- Recorded playback mode with ±5s skip buttons
|
||||
|
||||
**Metadata editing** *(requires Companion API)*
|
||||
- Single track editor with title, artist, album, album artist, track number, disc number, year
|
||||
- Per-album batch editor — edit all tracks in an album simultaneously
|
||||
- Multi-album batch editor — select multiple albums and apply changes across all of them
|
||||
- "Set same artist on all tracks" — fixes compilation albums split into per-artist entries in Navidrome
|
||||
- Custom album art — long-press any album art to replace it from your photo library
|
||||
|
||||
**Uploads** *(requires Companion API)*
|
||||
- Zip import from Files app with background URLSession uploading
|
||||
- Auto-tag on upload, Navidrome library rescan on completion
|
||||
|
||||
### watchOS
|
||||
|
||||
- Transfer songs from iPhone for standalone offline playback
|
||||
- Bluetooth headphone output
|
||||
- Apple Watch Ultra speaker mode via HKWorkoutSession
|
||||
- Digital Crown volume control with haptic feedback
|
||||
- Compact Mitsuha visualizer on the now playing screen
|
||||
- Tag edits sync automatically from iPhone via WatchConnectivity
|
||||
|
||||
### Debug Console
|
||||
|
||||
A built-in developer console toggleable from Settings:
|
||||
- **Docked panel** — appears above the tab bar with drag-to-resize (120–500pt)
|
||||
- **Group filter toggles** — iPhone / Watch / Companion / Audio, directly in the panel header, persisted to UserDefaults
|
||||
- **PiP mode** — floating draggable + resizable window, collapsible
|
||||
- **Full console sheet** — expand button opens level filters (ERROR / WARN / INFO / DEBUG), category pills, text search, delta timestamps, pause mode, and auto background/foreground separator lines
|
||||
- Export filtered logs via the share sheet
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
NavidromePlayer/
|
||||
├── Shared/ # Code shared between iOS and watchOS
|
||||
├── Shared/
|
||||
│ ├── API/SubsonicClient.swift # Full Subsonic/Navidrome REST client
|
||||
│ ├── Audio/AudioPlayer.swift # AVPlayer + AVAudioEngine, FFT, visualizer levels
|
||||
│ ├── Audio/AudioPlayer.swift # AVPlayer + AVAudioEngine, FFT, vis pipeline
|
||||
│ ├── Models/Models.swift # Codable models for all API responses
|
||||
│ └── Storage/
|
||||
│ ├── LibraryCache.swift # Disk cache for instant offline browsing
|
||||
│ ├── LibraryCache.swift # JSON disk cache for instant offline browsing
|
||||
│ ├── OfflineManager.swift # Download manager with progress tracking
|
||||
│ ├── ServerManager.swift # Multi-server with auto-failover
|
||||
│ └── WatchConnectivityManager.swift # WCSession file transfers
|
||||
│ └── WatchConnectivityManager.swift # WCSession file transfers + tag sync
|
||||
│
|
||||
├── iOS/
|
||||
│ ├── App/NavidromePlayerApp.swift # Entry point, background upload delegate
|
||||
│ ├── App/
|
||||
│ │ ├── NavidromePlayerApp.swift # Entry point, audio session, background upload
|
||||
│ │ └── NavidromeSceneDelegate.swift # UIWindow owner
|
||||
│ ├── Data/
|
||||
│ │ ├── AudioPreFetcher.swift # Pre-caches next 3 tracks (foreground only)
|
||||
│ │ ├── OptimisticActionQueue.swift # Offline action queue (stars, ratings)
|
||||
│ │ └── SyncEngine.swift # Library sync engine
|
||||
│ └── Views/
|
||||
│ ├── Common/ # MainTabView, MiniPlayer, AsyncCoverArt, DebugConsole
|
||||
│ ├── Companion/ # Companion API integration
|
||||
│ │ ├── CompanionAPIService.swift # API client + WebSocket push client
|
||||
│ │ ├── CompanionSettingsView.swift # Config, Smart DJ toggles, analysis triggers
|
||||
│ ├── Common/
|
||||
│ │ ├── MainTabView.swift # Root + mini player + docked debug panel
|
||||
│ │ ├── AsyncCoverArt.swift # Two-tier NSCache/disk image cache
|
||||
│ │ └── DebugConsoleView.swift # Developer console with PiP
|
||||
│ ├── Companion/
|
||||
│ │ ├── CompanionAPIService.swift # REST + WebSocket client
|
||||
│ │ ├── SmartCrossfadeManager.swift # Dual AVPlayer A/B crossfade engine
|
||||
│ │ ├── TrackEditorView.swift # Single-track metadata editor
|
||||
│ │ ├── BatchAlbumEditorSheet.swift # Edit tags for one album
|
||||
│ │ ├── MultiAlbumEditorSheet.swift # Batch edit across multiple albums
|
||||
│ │ ├── BatchUploadView.swift # Zip import + batch upload
|
||||
│ │ └── ZipImportManager.swift # Background URLSession uploads
|
||||
│ ├── Library/ # MyMusic, Albums, Artists, Playlists, Search, Radio, Downloads
|
||||
│ ├── Login/ # Server configuration
|
||||
│ ├── NowPlaying/ # Full player, SiriSeekBar, RadioStreamBuffer, Shazam
|
||||
│ └── Visualizer/ # MitsuhaVisualizerView, OfflineAudioAnalyzer, storage
|
||||
│ │ ├── TrackEditorView.swift
|
||||
│ │ ├── BatchAlbumEditorSheet.swift
|
||||
│ │ ├── MultiAlbumEditorSheet.swift
|
||||
│ │ ├── BatchUploadView.swift
|
||||
│ │ └── ZipImportManager.swift
|
||||
│ ├── Library/ # Albums, Artists, Songs, Playlists, Radio, Downloads
|
||||
│ ├── NowPlaying/
|
||||
│ │ ├── NowPlayingView.swift
|
||||
│ │ ├── SiriSeekBar.swift
|
||||
│ │ └── RadioStreamBuffer.swift
|
||||
│ └── Visualizer/
|
||||
│ ├── MitsuhaVisualizerView.swift # All rendering — wave, bar, line, siriwave
|
||||
│ ├── OfflineAudioAnalyzer.swift # Device-side FFT analysis pipeline
|
||||
│ └── VisualizerStorageManager.swift # VisFrameBuffer flat storage + memory-mapped I/O
|
||||
│
|
||||
├── watchOS/
|
||||
│ ├── App/ # Watch app entry, offline store, session manager
|
||||
│ ├── Audio/WatchAudioPlayer.swift # Dual mode: Bluetooth + Ultra speaker
|
||||
│ └── Views/ # Library, NowPlaying, Setup, Visualizer
|
||||
│ ├── App/ # Watch entry, offline store, WCSession manager
|
||||
│ ├── Audio/WatchAudioPlayer.swift # Bluetooth + Ultra speaker dual mode
|
||||
│ └── Views/ # Library, NowPlaying, Setup
|
||||
│
|
||||
├── project.yml # XcodeGen project definition
|
||||
├── project.yml # XcodeGen definition
|
||||
└── generate.sh # Regenerate .xcodeproj
|
||||
```
|
||||
|
||||
### Key design decisions
|
||||
|
||||
**`VisualizerLevelBox`** is a `fileprivate final class ObservableObject` with zero `@Published` properties, held by `@StateObject`. All mutations happen inside the SwiftUI Canvas closure — never through Combine. The visualizer updates at 60fps without triggering SwiftUI view diffing anywhere in the tree.
|
||||
|
||||
**`VisFrameBuffer`** stores pre-analyzed FFT frames as a flat `ContiguousArray<Float>` with memory-mapped I/O (`alwaysMapped`). `copyFrame(at:into:)` does a single contiguous copy into a pre-allocated destination — zero allocations per frame in the hot path.
|
||||
|
||||
**Background CPU** — four sources of background CPU were identified and eliminated: vis timers (30/60fps), the periodic time observer (10Hz SwiftUI re-evaluation), the offline FFT analysis Task (ran to completion with no cancellation), and SmartCrossfadeManager's independent 10Hz time observer. All four suspend on `didEnterBackground` and resume conditionally on `willEnterForeground`.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- **iOS 17.0+** / **watchOS 10.0+**
|
||||
- **Xcode 15+**
|
||||
- **iOS 26.0+** / **watchOS 26.0+**
|
||||
- **Xcode 26+**
|
||||
- **XcodeGen** — `brew install xcodegen`
|
||||
- **Navidrome server** — any version with Subsonic API support
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
git clone <repo>
|
||||
cd NavidromePlayer
|
||||
./generate.sh
|
||||
# Opens Xcode automatically. Select device and build.
|
||||
# Xcode opens. Select your device. Build.
|
||||
```
|
||||
|
||||
Set your Apple Developer Team ID in `project.yml` under `DEVELOPMENT_TEAM`.
|
||||
Set your Apple Developer Team ID in `project.yml` under `DEVELOPMENT_TEAM`. Change the bundle IDs if needed (`ca.dallasgroot.navidromeplayer.app`).
|
||||
|
||||
---
|
||||
|
||||
## Companion API
|
||||
|
||||
The Companion API is optional. Without it, the app works as a standard Navidrome player. With it, you get Smart DJ, tag editing, uploads, and server-side visualizer pre-computation.
|
||||
Completely optional. The app is fully functional without it. With it, you get Smart DJ crossfade, silence skipping, loudness normalization, tag editing, batch uploads, and server-precomputed visualizer frames.
|
||||
|
||||
### Server Directory Structure
|
||||
Runs as a Docker container alongside Navidrome — typically on the same Raspberry Pi or home server that already runs Navidrome.
|
||||
|
||||
### Server layout
|
||||
|
||||
```
|
||||
/home/pi/docker/navidrome/
|
||||
├── docker-compose.yml # Both Navidrome + Companion
|
||||
├── navidrome_data/ # Navidrome database (auto-created)
|
||||
├── companion_data/ # Persistent data (auto-created)
|
||||
│ ├── smart_dj.db # BPM, silence, loudness profiles
|
||||
│ └── vis_cache/ # Pre-computed Mitsuha FFT frames
|
||||
└── companion_api/ # Companion API source code
|
||||
├── docker-compose.yml
|
||||
├── navidrome_data/ # Navidrome database
|
||||
├── companion_data/
|
||||
│ ├── smart_dj.db # BPM, silence boundaries, LUFS profiles
|
||||
│ └── vis_cache/ # Pre-computed FFT frame files per song
|
||||
└── companion_api/
|
||||
├── Dockerfile
|
||||
├── main.py
|
||||
└── pre_analyze.py
|
||||
|
||||
/home/pi/navidrome/music/ # Your music library
|
||||
├── Artist/Album/song.flac
|
||||
└── ...
|
||||
/home/pi/navidrome/music/ # Your actual music files
|
||||
```
|
||||
|
||||
### docker-compose.yml
|
||||
|
|
@ -153,123 +282,59 @@ services:
|
|||
- navidrome
|
||||
```
|
||||
|
||||
> **Volume mount note:** The companion mounts `/home/pi/navidrome/music` directly (not the parent `/home/pi/navidrome`). This ensures `MUSIC_DIR=/music` maps to your actual music files without path prefix issues.
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
cd /home/pi/docker/navidrome
|
||||
|
||||
# ── Startup ──────────────────────────────────────────────
|
||||
docker compose up -d # Start everything
|
||||
docker compose up -d --build music-companion # Rebuild after code changes
|
||||
docker compose logs -f music-companion # View logs
|
||||
docker compose up -d --build music-companion # Rebuild after changes
|
||||
docker compose logs -f music-companion # Tail logs
|
||||
|
||||
# ── Health Check ─────────────────────────────────────────
|
||||
curl http://localhost:8000/health
|
||||
# Returns: profiles count, vis cache count, connected clients
|
||||
curl http://localhost:8000/health # Check status
|
||||
|
||||
# ── Pre-Analysis (run after adding new music) ────────────
|
||||
docker compose exec music-companion python pre_analyze.py # Analyze missing (DJ + vis)
|
||||
docker compose exec music-companion python pre_analyze.py --dj # DJ profiles only (faster)
|
||||
docker compose exec music-companion python pre_analyze.py --vis # Visualizer frames only
|
||||
# Pre-analyze your library — run after adding new music
|
||||
docker compose exec music-companion python pre_analyze.py # All missing tracks
|
||||
docker compose exec music-companion python pre_analyze.py --dj # DJ profiles only
|
||||
docker compose exec music-companion python pre_analyze.py --vis # Vis frames only
|
||||
docker compose exec music-companion python pre_analyze.py --force # Re-analyze everything
|
||||
|
||||
# ── Reset Analysis Data ─────────────────────────────────
|
||||
rm companion_data/smart_dj.db # Delete DJ profiles
|
||||
rm -rf companion_data/vis_cache/* # Delete vis frame cache
|
||||
docker compose exec music-companion python pre_analyze.py # Regenerate from scratch
|
||||
|
||||
# ── Maintenance ──────────────────────────────────────────
|
||||
docker compose restart music-companion # Restart API
|
||||
docker compose down # Stop everything
|
||||
docker compose down -v # Stop + remove volumes
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
### API endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/health` | Server status, profile count, vis cache count, connected clients |
|
||||
| `PATCH` | `/edit-metadata` | Edit ID3 tags (JSON body: `relative_path`, `title`, `artist`, `album`, etc.) |
|
||||
| `POST` | `/upload-track` | Upload audio file with metadata (multipart: `file`, `title`, `artist`, `album`) |
|
||||
| `GET` | `/smart-dj/profile?relative_path=...` | BPM, silence start/end, LUFS for a track |
|
||||
| `GET` | `/smart-dj/bulk-profiles?paths=...` | Batch fetch profiles (comma-separated paths) |
|
||||
| `GET` | `/visualizer/frames?relative_path=...` | Pre-computed Mitsuha FFT frames (JSON array) |
|
||||
| `POST` | `/visualizer/precompute` | Trigger background vis frame generation for all tracks |
|
||||
| `POST` | `/bulk-fix` | Trigger Navidrome library rescan |
|
||||
| `WS` | `/ws/push` | Real-time push events to iOS app |
|
||||
| `GET` | `/health` | Status, profile count, vis cache size, connected clients |
|
||||
| `PATCH` | `/edit-metadata` | Edit ID3 tags for one file |
|
||||
| `PATCH` | `/batch-edit-metadata` | Edit tags across multiple files |
|
||||
| `POST` | `/bulk-fix` | Apply fixes + trigger Navidrome rescan |
|
||||
| `POST` | `/upload-track` | Upload audio file with metadata (multipart) |
|
||||
| `GET` | `/smart-dj/profile?relative_path=` | BPM, silence start/end, LUFS for one track |
|
||||
| `GET` | `/smart-dj/bulk-profiles?paths=` | Batch fetch profiles |
|
||||
| `GET` | `/visualizer/frames?relative_path=` | Pre-computed FFT frame array |
|
||||
| `POST` | `/visualizer/precompute` | Queue background vis frame generation |
|
||||
| `WS` | `/ws/push` | Real-time push events to the iOS app |
|
||||
|
||||
### Path Resolution
|
||||
### iOS setup
|
||||
|
||||
Navidrome's `song.path` field can differ from the actual filesystem path. The Companion API uses a three-strategy resolver:
|
||||
1. **Direct join** — `MUSIC_DIR + relative_path`
|
||||
2. **Strip prefix** — removes leading components one at a time (handles Navidrome's library folder prefix)
|
||||
3. **Filename search** — walks the music directory looking for an exact filename match
|
||||
|
||||
If a 404 still occurs, the error response includes the exact paths that were tried for debugging.
|
||||
|
||||
### iOS App Configuration
|
||||
|
||||
1. **Settings** → **Companion API**
|
||||
2. Enter your server's IP address and port (default: 8000)
|
||||
3. Toggle **Enable Companion API**
|
||||
4. Tap **Test Connection** to verify
|
||||
5. Toggle **Smart DJ** for crossfade and analysis features
|
||||
1. **Settings → Companion API**
|
||||
2. Enter your server IP and port (default: `8000`)
|
||||
3. Toggle **Enable** → tap **Test Connection**
|
||||
4. Toggle **Smart DJ** for crossfade and analysis features
|
||||
|
||||
---
|
||||
|
||||
## Feature Details
|
||||
## Caching
|
||||
|
||||
### Mitsuha Visualizer
|
||||
Every view loads from cache first. The server updates the display silently in the background.
|
||||
|
||||
The visualizer renders smooth liquid waveforms using Catmull-Rom splines with configurable tension. FFT data comes from one of three sources:
|
||||
- **Real-time engine FFT** — AVAudioEngine tap on local files
|
||||
- **Offline pre-analyzed frames** — cached FFT data synced to playback position
|
||||
- **Server-computed frames** — downloaded from Companion API (saves device battery)
|
||||
|
||||
Settings include per-view configuration for Now Playing and Mini Player independently, with four built-in presets. The mini player visualizer automatically pauses when the full-screen Now Playing view is open (battery optimization).
|
||||
|
||||
### Radio
|
||||
|
||||
- **Playlist resolution** — resolves `.pls`, `.m3u`, `.asx` playlist URLs to direct stream URLs
|
||||
- **HLS detection** — identifies HLS streams from URL extension or content-type, disables raw buffering
|
||||
- **Timeshift** — buffer live radio and scrub back through recent audio
|
||||
- **Recording** — capture radio segments to local files
|
||||
- **Shazam** — identify currently playing content via SHSession + dedicated AVAudioEngine mic tap
|
||||
- **Recorded playback** — recorded radio files play with ±5s skip buttons instead of prev/next
|
||||
- **Live toggle** — start/stop buffering, snap to live edge
|
||||
|
||||
### Batch Tag Editing
|
||||
|
||||
Long-press any album to:
|
||||
- **Edit Album** — edit tags for all tracks in that album
|
||||
- **Select Albums** — enter multi-select mode (nav bar transforms: Cancel / count / Edit button), tap albums to select, "Select All" toggle, then apply changes across all selected albums
|
||||
|
||||
The "Set same artist on all tracks" toggle fixes compilation albums that split into separate per-artist entries in Navidrome.
|
||||
|
||||
### Downloads
|
||||
|
||||
Split into two sub-tabs:
|
||||
- **Offline** — storage usage, downloaded songs with playback, swipe to delete
|
||||
- **Watch** — Apple Watch connection status, pending transfers with progress, songs on watch, send all / delete all
|
||||
|
||||
### Debug Console
|
||||
|
||||
Toggle in Settings → Developer. Two display modes:
|
||||
- **Docked** — panel above the tab bar, drag handle to resize (120–500pt)
|
||||
- **PiP** — floating draggable/resizable window with collapse, dock-back, and close buttons
|
||||
|
||||
### Caching
|
||||
|
||||
All views use cache-first loading to eliminate spinners on warm launches:
|
||||
- **LibraryCache** — albums, artists, playlists, genres, album/artist details as JSON
|
||||
- **ImageCache** — two-tier NSCache + disk JPEG with 200MB limit and LRU eviction
|
||||
- **SmartDJCache** — Smart DJ profiles cached locally per song path
|
||||
- **AlbumCoverStore** — user-set custom album covers in Documents
|
||||
- **VisualizerStorageManager** — pre-analyzed FFT frames per song
|
||||
|
||||
Detail views load from cache instantly, refresh from server silently, and show a Retry button if both cache and server are unavailable.
|
||||
| Cache | Storage | Contents |
|
||||
|-------|---------|----------|
|
||||
| `LibraryCache` | JSON on disk | Albums, artists, playlists, genres, all-songs list, album/artist detail |
|
||||
| `ImageCache` | NSCache + JPEG on disk (200MB LRU) | Album art at multiple sizes |
|
||||
| `SmartDJCache` | In-memory | Smart DJ profiles per song path |
|
||||
| `AlbumCoverStore` | Documents directory | User-set custom album covers |
|
||||
| `VisualizerStorageManager` | Memory-mapped flat binary | Pre-analyzed FFT frames per song |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue