aboutsummaryrefslogtreecommitdiff
path: root/docs/design/vamp-music-player.org
blob: 12b92443beb7d55e1d1e69c1cf52f7a6a04cf840 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
#+TITLE: Design: VAMP — a standalone Emacs music player
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-22
#+OPTIONS: toc:nil num:nil

Status: Draft

VAMP = "VAMP Audio Music Player" (recursive backronym; /vamp/ is itself a
musical term — a short repeated passage). Namespace =vamp-=, repo =~/code/vamp=.

This design came out of a 2026-06-22 brainstorm. It supersedes parts of the
earlier EMMS-removal work and confirms others — see "Relationship to Prior
Work" below.

* Problem

=modules/music-config.el= (925 lines) is an EMMS configuration layer welded
into =.emacs.d=: it mixes genuinely reusable logic (M3U management, fuzzy add,
random-history navigation, radio-station creation, consume mode) with personal
config (ncmpcpp-aligned keybindings, paths, dashboard wiring), and it depends
on the EMMS package for its playlist model, player backend, and track info.
The goal is a standalone, publishable Emacs music player — derived from a
maintained subset of EMMS, depending on the EMMS package not at all — that
Craig uses as his primary player, launchable from Hyprland like dirvish.

* Relationship to Prior Work

A spec and a detailed review already exist and remain partly authoritative:

- =docs/specs/music-config-without-emms-spec.org= — the EMMS-removal spec.
- =docs/design/music-config-without-emms-review.org= — third-pass review
  (2026-05-15) with a go/no-go and a 14-item decision punch list.

This brainstorm *confirms* four of that review's decisions, independently
re-derived: long-running MPV + JSON IPC from day one (B1); a state-change hook
contract firing STARTED/STOPPED/PAUSED/RESUMED/FINISHED (B2); a fake-backend
testutil with an events ledger (B4); metadata via MPV IPC on the STARTED event
(S3).

It *pivots* the direction in four ways the prior work assumed otherwise:

- Publishable from the start (old spec: personal-first, public "later").
- Two adapters — MPV and mpd — behind a generalized adapter API (old: MPV-only,
  a single backend protocol). This is the largest change; it turns their single
  "backend protocol" into a real multi-backend seam.
- Name VAMP (old candidate: =cadenza=).
- Desktop integration as a first-class concern: a Hyprland Super+/ launcher, a
  daemon-singleton instance model, q-closes-frame-while-playback-continues, and
  an m3u MIME association — none of which the prior work addressed.

The prior review's cross-platform decision is absorbed unchanged: Linux + macOS
ship full-feature; Windows is best-effort (play/stop/next/previous only) per
Craig's 2026-05-15 call.

Next step (tracked below): revise the spec to this direction before
=/start-work=.

* Non-Goals

- No music-library database, tag index, or browser UI (light metadata only).
- No mid-track position resume on backend switch (v1 re-cues from track start).
- No persisted session state across daemon restarts (M3U save/load is the only
  way a playlist comes back).
- The package does not install OS wiring — the Hyprland bind, the launcher
  script, window rules, the =.desktop= file, and =xdg-mime= defaults all live
  in archsetup.
- No full tag-reading in v1 (deferred to the first post-v1 enhancement).

* Assumptions

Researched facts (verified this session):

- EMMS is GPLv3+ (read from =emms.el=); any code derived from it makes VAMP
  GPLv3+. Fine for MELPA, which prefers GPL.
- The EMMS core subset VAMP would draw from is ~6–7k lines: =emms.el= (1741),
  =emms-playlist-mode.el= (685), =emms-player-mpv.el= (772),
  =emms-player-mpd.el= (1367), the M3U sources (~800), native tag readers
  (~1080), playing-time (258). The ~16k excluded surface is browser, filters,
  tag-editor, lyrics, mpris, scrobblers, musicbrainz.
- The dirvish-popup / quick-capture launcher pattern (emacsclient named frame +
  Hyprland window rules + q-to-close, single-instance focus-existing) is the
  established model on Craig's machine.
- mpd is installed and running; Craig will use it to test the second adapter.

Assumptions to confirm before/early in build:

- mpd driven as a "dumb" single-file player (clear queue → add one file → play
  → idle for end-of-track) behaves cleanly. mpd is designed to own a queue;
  the dumb-player contract must be validated against real mpd behavior.
- The m3u XDG MIME association works on Craig's exact Hyprland/xdg setup
  (mechanism is standard; prototype the =.desktop= early per Craig's request).

* Approaches Considered

** Recommended: B/A hybrid — clean core, ported adapter internals

Write a small, fresh playlist/playback/navigation core with an adapter API of
VAMP's own design (B); port only the fiddly MPV-IPC and mpd-protocol internals
from EMMS as reference (A), since that protocol handling is the hard-won part
not worth reinventing. The core owns the queue and all play-modes; adapters are
thin single-file players.

Pros: a small core Craig fully understands and can maintain solo; a clean
adapter API shaped for the two-backend goal; reuses EMMS's proven IPC/protocol
code without inheriting its whole design.

Cons: GPLv3+ (from the ported adapter code); real upfront design effort on the
core + adapter API before any feature lands; risk of missing subtle
player-process lifecycle behavior EMMS already handles.

What it trades away: the option of a non-GPL license, and a fast feature-first
start.

** Rejected: vendor-and-trim (A alone)

Copy the ~8 core EMMS files, delete the rest, renamespace, keep EMMS's backend
pattern as VAMP's own. Fastest to feature-complete, but inherits 6–7k lines of
someone else's idioms to "maintain yourself" — works against the maintainability
goal that motivated the project.

** Rejected: thin core, delegate to backends (C)

Lean hard on the backend (mpd owns its queue; MPV gets a minimal one). Least
code, but backend asymmetry leaks into inconsistent behavior and fat adapters —
and the brainstorm chose core-owns-queue precisely for uniform behavior and
seamless backend switching. C survives only as an adapter-capability detail
(let mpd do server-side work as a future optimization).

** Rejected: wrap existing client libraries (D)

Build on mpdel + mpv.el as a thin UX layer. Directly contradicts the
"depend on nothing / maintain it myself" goal.

** Rejected: MPRIS/D-Bus as the one universal adapter (E)

Drive any MPRIS player over D-Bus. "Many players" almost free, but MPRIS is
control-only — it can't reliably own playlists or load arbitrary files across
players. Kept in the back pocket as a possible future adapter class, not a v1
foundation.

** Rejected: external daemon + JSON-RPC (F)

Move player logic to an external process, Emacs as thin client. Ships a
non-Elisp component — packaging burden, not a pure-Elisp MELPA package. Overkill
for local playback.

* Design

** Architecture

Standalone repo at =~/code/vamp=, Eask-based like pearl (Eask, Makefile,
autoloads, =tests/=, README, LICENSE — GPLv3+). Three layers:

- *Core* (backend-agnostic, owns all stateful logic): the queue model (track
  list, current index, play-modes — shuffle, repeat-playlist, repeat-track,
  random + history ring, consume); the playback controller (orchestrates
  load + play on the current track, handles end-of-track, advances per mode);
  sources (add files/dirs/recursive, URLs, M3U load/save/clear/reload/edit;
  radio-station creation); the playlist-mode buffer + window toggle/show;
  light metadata.
- *Adapter layer* (the extensibility seam): a =cl-defgeneric= protocol every
  backend implements. Ships with MPV (spawned subprocess + JSON IPC socket) and
  mpd (daemon connection, driven as a dumb single-file player).
- *=.emacs.d= glue* (=vamp-config.el=): keybindings (C-; m map, playlist-mode
  keys), music-root path, dashboard wiring, customize values. No logic.

Three-project split: VAMP ships the elisp + entry points; =.emacs.d= keeps
keybindings/glue; archsetup owns the OS wiring (Super+/ bind, launcher script,
window rules, =.desktop=, =xdg-mime=).

** Adapter API

A backend is a class implementing generic methods. The contract is deliberately
narrow (transport + metadata), because the core owns the queue and modes:

- =load-file= — load a track URL/path (do not advance anything)
- =play= / =pause= / =stop=
- =seek= — to an absolute or relative position
- =position= — current playback position
- =report-metadata= — title/artist/album/duration the backend knows about
- an *end-of-track notification* — each adapter translates its native
  "track finished" signal (MPV: the IPC =end-file= event; mpd: the idle
  =player= subsystem) into one uniform core callback

This is the review's B2 state-change contract, generalized across backends. A
new backend is a new class + method implementations; nothing in the core
changes.

** Backend switching

The payoff of core-owns-queue: the queue and current track are backend-agnostic
state in the core, so a runtime switch is just — stop the outgoing adapter
(kill the MPV subprocess / drop the mpd connection), set the active adapter,
and the next play re-issues =load-file= to the new backend on the same current
track. Nothing in the queue moves; the selected song stays selected. An
interactive =vamp-switch-backend= command (completing-read over self-registered
adapters) is bound under C-; m and in playlist-mode. v1 re-cues from track
start on switch; mid-track position-resume is a post-v1 addition (the contract
already has =seek=, so it's additive).

** Data flow / control loop

A user action (play / next / previous) updates queue state in the core (current
index advanced per the active play-mode), then the core calls the active
adapter's =load-file= + =play=. End-of-track is the one hard cross-backend
signal: the adapter fires the uniform callback, and the core's handler consults
the play-mode and advances — repeat-track replays, repeat-playlist wraps, random
pushes history and picks next, consume drops the finished track, normal advances
or stops at the end. A track is a struct (url/path + type slot + cached light
metadata); the queue is an ordered track list + current index + mode flags + the
random-history ring.

** Presentation / faces

Every stateful UI surface gets a named =defface=, so status is shown by face,
not hardcoded color: playlist current/played/consumed lines and metadata
columns; play state (playing/paused/stopped); each play-mode with a lit (on) and
dimmed (off) face; the active-backend indicator (MPV vs mpd); backend
health (e.g. mpd-disconnected as an error face). These render in a header-line
status strip in the playlist buffer (and feed the mode-line); the mode/transport
indicators light via their on-face and dim via their off-face.

Base palette: faces ship with defaults that *inherit from standard Emacs faces*
(=success=, =warning=, =error=, =shadow=, =highlight=, =font-lock-*=) so they
look right under any user theme out of the box and adapt automatically. A
separate, optional =vamp-theme.el= carries the opinionated palette. Every face
stays individually overridable. The selected-track line uses a single reused
overlay repositioned on each STARTED event (review B3).

Testing the palette: because they're standard deffaces, theme studio (the
=.emacs.d= tool) renders them directly — load the VAMP faces, preview the
playlist buffer + status strip, check legibility against the modus contrast
targets, iterate.

** Desktop integration + instance model

Launcher: a =vamp-popup= script (mirror of dirvish-popup) bound to Super+/ in
Hyprland; ncmpcpp moves to Shift+Super+/. The script focuses an existing
"vamp" frame if one is open, else spawns a floating frame running
=(vamp-popup)=; Hyprland window rules float/size/center the "vamp"-named frame.

Instance model: one player instance = the daemon's global state (queue, active
adapter, the live MPV subprocess / mpd connection). Super+/ attaches a view
frame to it. q closes that frame but *playback continues in the daemon* — close
the window, music plays on, reopen to see it again. A separate command (or Q)
fully stops and tears down the player. The launcher's focus-existing behavior
enforces an at-most-one view frame, so there are no competing instances. The
non-daemon case (standalone Emacs) is its own instance — an edge case, since
Craig runs the daemon.

m3u MIME association: a =.desktop= file with
=MimeType=audio/x-mpegurl;audio/mpegurl;application/x-mpegurl;application/vnd.apple.mpegurl=
and =Exec=music-open %f= (wrapper → emacsclient … =(vamp-open-m3u "%f")=), then
=xdg-mime default=. Opening any =.m3u= from a file manager or =xdg-open= then
launches/raises VAMP and loads that playlist. The package only needs the
=vamp-open-m3u FILE= entry point; the =.desktop= + =xdg-mime= live in archsetup.

** Persistence

Playlists: M3U save/load/clear/reload/edit, file-based, same as today. No
session state — each daemon start is empty (today's behavior).

** Metadata

v1: adapter-reported metadata for the playing track only (MPV =get_property
metadata= on STARTED; mpd reports tags from its DB). The playlist shows
filename/path-derived labels (today's =track-description= behavior); the current
track shows the real title/duration the backend reports. Post-v1: vendor
=emms-info-native= (~1080 lines; mp3/ogg/flac) for real artist/album tags across
the whole playlist, which is what unlocks sort-by-tag.

** Error handling

Failures surface via =user-error= / =message=, never silently — the
music-config history (the silent Slack-notify and lock-screen bugs) is the
cautionary tale. A missing/dead backend (mpd not running, mpv binary absent)
reports clearly and is reflected in the header-line health face.

** Testing

The adapter is the system boundary (subprocess / IPC / network), so that is the
only thing mocked — never the core. A *test adapter* (null backend, review B4's
=testutil-music-backend.el=) implements the protocol, records
=load-file=/=play=/=stop= calls in an events ledger, and lets a test fire the
end-of-track callback on demand. With it, the entire control loop and every
play-mode is testable as pure logic — no MPV, no mpd, no audio. This is
dependency-injection rather than primitive-mocking, which also sidesteps the
native-comp subr-mock trap the suite recently fought (see
=docs/native-comp-subr-mocking.org=): a fake adapter is injected, not a subr
=cl-letf='d. Core-logic tests (queue, navigation, M3U parse/write, fuzzy add,
source expansion) are largely the existing ~193 music-config tests, ported with
renamed symbols. Per-adapter tests mock the IPC socket / protocol connection and
assert the native-event → uniform-callback translation. One or two integration
tests drive the real core through the test adapter.

** Observability

A debug log buffer captures raw adapter I/O (the IPC/protocol traffic) for
diagnosing backend issues (EMMS has this for mpv; worth keeping). State changes
surface in the header-line + mode-line. A =vamp-doctor= command reports backend
availability and, on Windows, the degraded-mode limitation.

** Cross-platform stance

Linux + macOS ship full-feature (IPC over unix domain sockets). Windows is
best-effort — play/stop/next/previous only, no pause/seek/volume — via
=start-process= + stdin or one-shot =call-process=, because Emacs's
=make-network-process= doesn't natively support Windows named pipes. Documented
in the README and surfaced by =vamp-doctor=. (Craig, 2026-05-15.)

* Open Questions

- [ ] mpd dumb-single-file-player contract — validate that clear-queue → add →
  play → idle-for-end behaves cleanly against real mpd; decide the exact command
  sequence. Candidate for an early spike.
- [ ] Exact mpd end-of-track signal handling (idle =player= vs polling) and how
  it maps to the uniform callback without races.
- [ ] =.desktop= + =xdg-mime= prototype on Craig's Hyprland setup — confirm m3u
  opens VAMP early (Craig asked to de-risk this first).
- [ ] Floating-frame geometry / Hyprland window rules for the "vamp" frame
  (archsetup detail).
- [ ] v1 parity catalog — confirm the 13 EMMS features from the review's S1 all
  carry into the playlist-mode keymap (seek, volume, one-shot shuffle, info,
  center, kill-track, bury, append-to-M3U, active-window tint, dired/dirvish
  add).

* Next Steps

- *Reconcile the spec.* Revise =docs/specs/music-config-without-emms-spec.org=
  to this direction — publishable-now, two adapters + generalized adapter API,
  VAMP name, desktop integration + instance model — keeping the review's
  confirmed B1/B2/B4/S3 decisions and the 14-item punch list still relevant.
- *Spike the risky assumptions* (the two mpd open questions, the m3u =.desktop=)
  before committing the adapter API shape.
- Open questions that are genuine decisions → =arch-decide= as ADRs.
- Implementation → =/start-work= against the revised spec; pure-helper
  extraction (review's Migration Plan step 1) is the safe first phase and can
  start independently.
- Link this doc from the =todo.org= task "Extract music-config into a standalone
  plugin."