aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-15 02:09:23 -0500
committerCraig Jennings <c@cjennings.net>2026-05-15 02:09:23 -0500
commitd9fa8f4db2b9e6d7f610094950b460cdee146e47 (patch)
tree99ce7c91e18865f193117592cd9f3804992dbd83 /docs
parentf6ed94cf36cdb81ac8b33f5118e9ec978d3dcd77 (diff)
downloaddotemacs-d9fa8f4db2b9e6d7f610094950b460cdee146e47.tar.gz
dotemacs-d9fa8f4db2b9e6d7f610094950b460cdee146e47.zip
docs(design): commit music-config-without-emms spec + readiness review
The spec lays out the EMMS-removal design: package-owned track and playlist structs, a narrow backend protocol with mpv as the v1 backend, state-change hooks replacing EMMS player hooks, an overlay-based selected-track marker, a fake-backend test architecture, a quantified performance budget, a 22-step parity walk, and the migration plan. The review tracks implementation readiness: which migration-plan step is safe to start, which decisions still block the rest, and the exact spec edits required. Two decisions landed this session and are now baked into the spec: - Platform support: Linux and macOS get full features; Windows runs in degraded mode (play/stop/next/previous only) because Emacs cannot natively connect to mpv's Windows named-pipe IPC. Anyone who wants full Windows parity can wire mpvc.exe shellout or a w32-* named-pipe client as a follow-up. - File-extension scope: cj/music-file-extensions stays as-is. webm and ape files in ~/music are intentionally skipped. Socket path now references temporary-file-directory instead of a hardcoded /tmp/ prefix so the spec stays consistent with the Windows section.
Diffstat (limited to 'docs')
-rw-r--r--docs/design/music-config-without-emms-review.org299
-rw-r--r--docs/design/music-config-without-emms.org543
2 files changed, 842 insertions, 0 deletions
diff --git a/docs/design/music-config-without-emms-review.org b/docs/design/music-config-without-emms-review.org
new file mode 100644
index 00000000..67ef0d1b
--- /dev/null
+++ b/docs/design/music-config-without-emms-review.org
@@ -0,0 +1,299 @@
+#+TITLE: Review: music-config Without EMMS -- Implementation Readiness
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-05-15
+#+OPTIONS: toc:nil num:nil
+
+Spec reviewed: =docs/design/music-config-without-emms.org=
+Third pass. This review answers the single question: is the spec in
+shape to start =/start-work= against?
+
+Prior reviews (architectural critique, test/perf/parity/packaging
+analysis) are in git history and remain accurate. This pass
+distills them into a go / no-go and a punch list of exactly what
+must change before implementation starts.
+
+* Verdict
+
+**No, not quite.** Step 1 of the Migration Plan can start today.
+Steps 2-7 need five interlocked decisions resolved in the spec first.
+Without them, the implementer would be making those decisions
+mid-flight -- exactly when the cost of being wrong is highest.
+
+Estimated spec work to reach implementable: **1-2 hours of focused
+editing.** No new analysis needed; the answers are already in the
+prior reviews.
+
+* What is implementable today
+
+The pure-helper extraction (Migration Plan step 1) does not depend on
+any of the blocking decisions. Specifically:
+
+- =cj/music--valid-file-p= (with =webm= and =ape= added per the prior format audit)
+- =cj/music--valid-directory-p=
+- =cj/music--collect-entries-recursive=
+- =cj/music--completion-table=
+- =cj/music--m3u-file-tracks=
+- =cj/music--get-m3u-files= / =--get-m3u-basenames=
+- =cj/music--safe-filename=
+- =cj/music--format-duration=
+- =cj/music--append-track-to-m3u-file=
+- =cj/music--assert-valid-playlist-file=
+- =cj/music-create-radio-station=
+
+These are already EMMS-free in spirit; their tests already pass
+without EMMS mocking. Extracting them is roughly a 2-3 day commit
+with characterization tests as the safety net.
+
+That work can start before the spec revisions land. But it should
+not be open-ended -- it is one phase, after which work stops until
+the spec is revised.
+
+* What is blocking the rest
+
+Five interlocked decisions. Each is small to resolve; the cost of
+not resolving them is large.
+
+** B1: mpv IPC transport
+
+Spec lines 125-131 punt on this with "Pause/seek/volume can use
+mpv's JSON IPC socket if available; otherwise v1 may support only
+play/stop/next/previous and leave pause/seek/volume as explicit
+follow-up work."
+
+The fallback path produces a worse player than today's EMMS+mpv
+(which has working pause). Per-track-restart pause is an 80-200 ms
+spawn cost plus the loss of the playing position. That is a
+user-visible regression.
+
+**Decide**: long-running mpv + JSON-IPC socket from day one.
+
+This blocks Migration Plan step 4 (mpv backend).
+
+** B2: state-change hook contract
+
+Today's module subscribes to six EMMS hooks (=started=, =stopped=,
+=paused=, =finished=, =cleared=, plus advice on the three toggle
+commands). The header overlay, consume-after-finish, random-history,
+auto-advance, and modeline updates all depend on them.
+
+Spec lists these hooks under "Existing EMMS Coupling" but the
+Proposed Architecture has no replacement -- the backend protocol on
+spec lines 100-111 is read-side only.
+
+**Decide**: abnormal hook =cj/music-state-change-functions= fired by
+the backend with a plist =(:event STARTED|STOPPED|PAUSED|RESUMED|FINISHED
+:track TRACK :error MAYBE)=. Five events. Dispatched from the mpv
+IPC process filter on the corresponding mpv events
+(=playback-restart=, =pause=, =end-file=).
+
+This blocks Migration Plan step 5 (rewire public commands). Without
+it, "rewire" has no API to rewire to.
+
+** B3: selected-track representation
+
+Spec proposes =selected-index= in the playlist struct -- fine for
+state. But the buffer view still needs a marker / overlay / face on
+the playing line. The current code uses
+=emms-playlist-selected-marker= (a buffer marker) plus
+=emms-playlist-selected-face=.
+
+**Decide**: overlay at the line whose index matches
+=selected-index=, face =cj/music-playlist-selected-face=, repositioned
+on each =STARTED= event. Single overlay reused across track
+changes.
+
+This blocks playlist-view rendering and reorder behavior.
+
+** B4: test architecture
+
+~110 of the 252 existing tests mock EMMS at the boundary (=emms-next=,
+=emms-random=, =emms-playlist-track-at=, =emms-playlist-mode-kill-track=,
+etc.). Without a shared fake-backend testutil, the rewrite happens
+110 times in 110 different shapes.
+
+**Decide**: =tests/testutil-music-backend.el= provides
+=cj/test-music-fake-backend= (matching the real backend's plist
+shape) plus an =:events= ledger that records each call as
+=(verb . args)=. Tests bind =cj/music-current-backend= to the fake
+and assert sequences against the ledger. Build this first; rewrite
+tests against it second.
+
+Without this, effort balloons by roughly a factor of three.
+
+** B5: cross-platform stance
+
+mpv's IPC works over unix domain sockets on Linux+macOS but uses
+named pipes on Windows. Emacs's =make-network-process= does not
+natively support Windows named pipes.
+
+**Decided this session (Craig, 2026-05-15)**: **best-effort on
+Windows.** v1 ships full-feature on Linux + macOS; Windows gets a
+degraded mode with play / stop / next / previous only (no pause, no
+seek, no volume) via =start-process= + stdin or one-shot
+=call-process= per command. The README documents the limitation.
+Anyone who wants to fund Option B (=mpvc.exe= shellout) or Option C
+(=w32-*= named-pipe client) can do it as a follow-up.
+
+This unblocks the implementer to write Linux+macOS code without
+spending the v1 budget on Windows IPC plumbing. Quote Craig in
+the spec body so the rationale survives.
+
+* Smaller items the implementer should not have to invent
+
+** S1: feature-parity scope
+
+13 EMMS features are missing from the spec entirely (catalogued in
+the prior parity review): seek forward (=f=), seek backward (=b=),
+volume up (=+= / ===), volume down (=-=), one-shot shuffle (=Z=),
+info popup (=i=), center-on-current (=o=), kill-track (=d= or
+similar), bury buffer (=q=), append-to-M3U (=A=), active-window
+background tint, dired/dirvish add (=+= in dired).
+
+**Decide**: preserve all 13 in the playlist-mode keymap. Volume and
+seek depend on B1 (IPC); if B1 is decided in favor of IPC, they all
+come along for free. Dired/Dirvish add is independent and worth
+explicitly listing in the Goals section.
+
+** S2: URL-vs-file routing
+
+Today's mpv player detects URLs vs files via
+=emms-player-mpv-regexp=. Spec does not carry this forward.
+
+**Decide**: small helper
+
+#+begin_src elisp
+(defun cj/music--track-type-from-name (name)
+ (cond ((string-match-p "\\`\\(?:https?\\|mms\\)://" name) 'url)
+ ((cj/music--valid-file-p name) 'file)
+ (t nil)))
+#+end_src
+
+Used when constructing tracks; populates the existing =type= slot.
+
+** S3: metadata strategy
+
+Track struct includes =title=, =artist=, =duration= but the spec
+says "tag extraction can be added later."
+
+**Decide one of two**:
+
+- Drop the metadata fields from v1's struct. Track description
+ falls back to filename-without-extension (matches current
+ =cj/music--track-description= behavior for untagged files).
+- Keep the fields; populate via mpv IPC =get_property metadata= on
+ the =STARTED= event. Lazy, free, fits the IPC story from B1.
+
+Recommend the second. It's two lines of code given B1's IPC
+infrastructure is already there.
+
+** S4: scope declaration
+
+Spec is ambiguous on whether this stays a personal module or ships
+as a public package.
+
+**Decide**: personal Phase 1 (=modules/music-config.el=, =cj/=
+namespace). Re-evaluate Phase 3 (=cadenza=, GitHub, MELPA) after
+~4 weeks of daily use. Quote Craig's preference for this phasing in
+the spec body.
+
+* The exact spec edits
+
+Concrete inserts and deletions, in order. Roughly 1-2 hours of
+focused work, no new analysis.
+
+1. **Rewrite "Initial Backend"** (spec lines 118-133) -- drop the
+ "if available" / "otherwise" fork. Single decision: long-running
+ mpv with =--input-ipc-server=<PID-stamped socket under
+ =temporary-file-directory=>=, JSON commands over
+ =make-network-process :family 'local=, async event filter on the
+ socket's process buffer.
+
+2. **Insert "Process Lifecycle"** subsection after "Initial
+ Backend" -- one mpv per Emacs session, killed on =kill-emacs-hook=,
+ socket file removed in the same hook. Stale-socket cleanup on
+ startup (loop =/tmp/cj-music-mpv-*.sock= or the platform
+ equivalent and unlink).
+
+3. **Insert "State-Change Hooks"** subsection after "Backend
+ Protocol" -- =cj/music-state-change-functions= abnormal hook
+ with the 5-event plist contract. List the mpv events that
+ trigger each.
+
+4. **Insert "Selected-Track Representation"** subsection in the
+ Playlist Buffer section -- single overlay at the line whose
+ track-index matches =selected-index=, face
+ =cj/music-playlist-selected-face=, repositioned on the =STARTED=
+ event.
+
+5. **Insert "Test Architecture"** subsection at the top of the
+ Migration Plan -- =testutil-music-backend.el=, fake-backend plist
+ shape, events ledger, characterization tests for the six
+ user-facing commands (=next=, =previous=, =toggle-consume=,
+ =playlist-toggle=, =playlist-load=, =playlist-clear=) before any
+ non-pure code moves.
+
+6. **Insert "Platform Support"** subsection -- quote Craig's
+ 2026-05-15 decision: full features on Linux + macOS; Windows is
+ best-effort with play/stop/next/previous only. Document in the
+ README and surface in =M-x cj/music-doctor= when run on Windows.
+
+7. **Rewrite the track struct definition** (spec line 72-79) per
+ S3 -- either drop metadata fields or keep with a one-line note
+ about lazy population via mpv IPC.
+
+8. **Expand "Goals"** to list the 13 missing features per S1 with
+ "preserve" disposition. Mark volume / seek as
+ "preserve if B1=IPC, drop otherwise" -- but B1 is IPC, so they
+ all stay.
+
+9. **Insert URL-detection helper** (S2) in the M3U Handling section
+ or alongside the track struct. Cite where it's used.
+
+10. **Insert "Scope"** subsection before Goals -- this is a personal
+ module first; potential public package later. Quote Craig's
+ preference. Name the future public name candidate (=cadenza=).
+
+11. **Update "Existing EMMS Coupling"** inventory with the surfaces
+ missed in the prior review (kill-track, shift-track, bury,
+ selected-marker, =emms-source-file-default-directory=,
+ =emms-player-mpv-parameters=, =emms-player-mpv-regexp=,
+ =emms-track-description-function=, the custom-set-faces block,
+ and the =emms-all= / =emms-mode-line-mode= setup calls).
+
+12. **Strengthen "Acceptance Criteria"** with a parity-walk entry
+ (the 22-step user workflow from the prior review).
+
+13. **Add "Effort estimate"** and "Risks" sections to match the
+ format of the other =docs/design/= specs.
+
+14. **Add ~webm~ and ~ape~** to =cj/music-file-extensions= (or note
+ that the migration step does so). Two extensions, two-line
+ edit; the audit confirms files of both types exist in =~/music=.
+
+* What stays as-is
+
+The spec's data model (track struct, playlist struct), backend
+plist shape, M3U-as-persistence stance, pure-helpers-first migration
+ordering, and the overall layering (data / backend / view) are all
+sound. Don't rework them. The fixes above are inserts and
+clarifications, not architectural changes.
+
+* Recommended next step
+
+Two paths, pick one:
+
+1. **Apply the 14 edits directly.** I draft them as concrete inserts
+ into the spec, you review the final spec end-to-end, then
+ =/start-work= against the revised spec. Roughly the same 1-2
+ hours but spent on review rather than writing.
+
+2. **You apply the edits.** This review is the punch list. Work
+ through items 1-14, ping me when ready, I do one more pass to
+ verify, then =/start-work=.
+
+Either way, the next milestone is "the spec passes Phase 3 (Approach)
+of =/start-work= without me having to invent the missing decisions
+on the fly."
+
+Until then, only the pure-helper extraction (Migration Plan step 1)
+is safe to start.
diff --git a/docs/design/music-config-without-emms.org b/docs/design/music-config-without-emms.org
new file mode 100644
index 00000000..929423df
--- /dev/null
+++ b/docs/design/music-config-without-emms.org
@@ -0,0 +1,543 @@
+#+TITLE: Design: music-config Without EMMS
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-05-15
+
+* Status
+
+Specification only. No implementation has been started.
+
+Effort: Large. This is a multi-week module rewrite involving process
+management, player state, playlist state, a playlist major mode, and updates
+across the existing music test suite.
+
+* Problem
+
+=modules/music-config.el= is currently an EMMS configuration module, not an
+independent music module. The useful workflows are local to this config, but
+the core state lives in EMMS: the playlist buffer is the source of truth, player
+state is reported through EMMS hooks, and track metadata is read through EMMS
+track objects.
+
+That shape makes the module harder to test and evolve than it needs to be.
+Simple playlist operations have to load or stub EMMS, and UI refresh,
+consume-on-finish, random history, and auto-advance are all coupled to EMMS
+player hooks.
+
+The target design is a standalone music module that owns playlist state,
+controls =mpv= directly through a small backend protocol, and renders a
+playlist buffer as a view over package-owned data.
+
+This remains a personal config module in =modules/music-config.el=. It should
+be internally coherent enough that it could later become a package, but v1 does
+not target MELPA or a public package boundary.
+
+* Goals
+
+- Keep the current user workflows: fuzzy add, recursive directory add,
+ Dired/Dirvish add, M3U load/save/edit/reload, append-track-to-M3U, radio
+ station creation, playlist window toggle, random/repeat/single/consume
+ controls, track reordering, and mpv playback.
+- Make EMMS unnecessary for =music-config.el= load, tests, and normal use.
+- Separate domain logic from playback and UI so helpers stay easy to test.
+- Replace EMMS-bound keymap entries with =cj/music-*= commands.
+- Keep playlist files portable M3U files.
+
+* Non-Goals
+
+- Reimplementing the full EMMS feature set.
+- Building a music library database, tag editor, or metadata indexer.
+- Supporting multiple player daemons in v1.
+- Supporting album art, lyrics, queue persistence across Emacs restarts, or
+ remote control protocols beyond mpv.
+- Publishing a standalone =cj-music= package, adding MELPA metadata, or
+ converting all personal configuration variables to public =defcustom= forms.
+
+* Existing EMMS Coupling
+
+The current module depends on EMMS in these areas:
+
+- Playback commands: =emms-pause=, =emms-stop=, =emms-next=,
+ =emms-previous=, =emms-start=, =emms-random=,
+ =emms-seek-forward=, =emms-seek-backward=, =emms-volume-raise=,
+ =emms-volume-lower=, and shuffle/repeat/random toggles.
+- Add/load/save commands: =emms-add-file=, =emms-add-directory-tree=,
+ =emms-play-playlist=, =emms-playlist-save=,
+ =emms-source-playlist-ask-before-overwrite=, and
+ =emms-playlist-clear=.
+- Playlist buffer operations: =emms-playlist-buffer=,
+ =emms-playlist-mode=, =emms-playlist-track-at=,
+ =emms-playlist-current-selected-track=, =emms-playlist-select=,
+ =emms-playlist-mode-go=, =emms-playlist-mode-bury-buffer=,
+ =emms-playlist-mode-center-current=,
+ =emms-playlist-mode-shift-track-up=,
+ =emms-playlist-mode-shift-track-down=,
+ =emms-playlist-mode-kill-track=, and
+ =emms-playlist-selected-marker=.
+- Track representation: =emms-track-name=, =emms-track-type=,
+ =emms-track-get=, =emms-track-simple-description=, and
+ =emms-track-description-function=.
+- Lifecycle hooks and state: =emms-player-started-hook=,
+ =emms-player-stopped-hook=, =emms-player-paused-hook=,
+ =emms-player-finished-hook=, =emms-playlist-cleared-hook=,
+ =emms-player-playing-p=, =emms-player-paused-p=,
+ =emms-random-playlist=, =emms-repeat-playlist=, and
+ =emms-repeat-track=.
+- EMMS setup: =emms-all=, =emms-mode-line-mode=,
+ =emms-playing-time-disable-display=, =emms-source-file-default-directory=,
+ =emms-playlist-default-major-mode=, =emms-player-list=,
+ =emms-player-mpv-parameters=, =emms-player-mpv-regexp=, and EMMS playlist
+ faces.
+
+Anything outside those areas, especially file discovery, safe filename
+generation, M3U parsing, and radio station file creation, can remain mostly
+unchanged.
+
+* Proposed Architecture
+
+** Data Model
+
+Introduce a package-owned track model:
+
+#+begin_src emacs-lisp
+(cl-defstruct cj/music-track
+ type ; 'file or 'url
+ name ; absolute file path or stream URL
+ title
+ artist
+ duration)
+#+end_src
+
+The =title=, =artist=, and =duration= slots are populated opportunistically from
+mpv IPC metadata after a track starts. V1 must still behave correctly when
+metadata is absent by displaying a filename or decoded URL.
+
+Introduce playlist state that belongs to this package:
+
+#+begin_src emacs-lisp
+(cl-defstruct cj/music-playlist
+ tracks
+ selected-index
+ file
+ repeat-playlist
+ repeat-track
+ random
+ consume
+ random-history)
+#+end_src
+
+The playlist buffer should render this state. It should not be the source of
+truth. Buffer text becomes a view over =cj/music-current-playlist=.
+
+Track construction should use a small type helper instead of EMMS's mpv regex:
+
+#+begin_src emacs-lisp
+(defun cj/music--track-type-from-name (name)
+ (cond ((string-match-p "\\`\\(?:https?\\|mms\\)://" name) 'url)
+ ((cj/music--valid-file-p name) 'file)
+ (t nil)))
+#+end_src
+
+** Read-Side State API
+
+UI code should read player and playlist state through package-owned helpers,
+not through backend internals:
+
+- =cj/music-playing-p=
+- =cj/music-paused-p=
+- =cj/music-current-track=
+- =cj/music-playlist-state=
+- =cj/music-track-description=
+
+The playlist header, modeline indicators, and tests should use these helpers.
+
+** Backend Protocol
+
+Playback should go through a narrow backend plist:
+
+#+begin_src emacs-lisp
+(:name 'mpv
+ :available-p cj/music-mpv-available-p
+ :play cj/music-mpv-play
+ :pause cj/music-mpv-pause
+ :resume cj/music-mpv-resume
+ :stop cj/music-mpv-stop
+ :seek cj/music-mpv-seek
+ :volume cj/music-mpv-volume
+ :status cj/music-mpv-status
+ :metadata cj/music-mpv-metadata)
+#+end_src
+
+The module should provide commands such as =cj/music-play=,
+=cj/music-pause=, =cj/music-stop=, =cj/music-next=, =cj/music-previous=,
+=cj/music-seek-forward=, =cj/music-seek-backward=,
+=cj/music-volume-raise=, and =cj/music-volume-lower=. Those commands operate
+on package playlist state and then call the selected backend.
+
+** State-Change Hooks
+
+Replace EMMS player hooks with one package-owned abnormal hook:
+
+#+begin_src emacs-lisp
+(defvar cj/music-state-change-functions nil
+ "Abnormal hook run when music player state changes.
+Each function receives a plist:
+(:event EVENT :track TRACK :error ERROR).")
+#+end_src
+
+Events for v1:
+
+- =started=
+- =paused=
+- =resumed=
+- =stopped=
+- =finished=
+- =error=
+- =playlist-changed=
+- =mode-changed=
+
+The mpv backend is responsible for dispatching player events from process
+sentinels and IPC event messages. Package features should subscribe here:
+
+- Header refresh runs on every event.
+- Random history records on =started= when random mode is active.
+- Consume mode removes the finished track on =finished=.
+- Auto-advance runs on =finished= unless playback was deliberately stopped.
+- Playlist-file reset runs on =playlist-changed= when the playlist is cleared.
+
+** mpv Backend
+
+V1 should use mpv JSON IPC from the start. Pause, seek, and volume are core
+workflow parity, and implementing them later would leave a worse player than
+the current EMMS+mpv setup.
+
+Spawn mpv with:
+
+#+begin_src sh
+mpv --no-video --quiet --audio-display=no \
+ --input-ipc-server=<SOCKET-PATH> TRACK
+#+end_src
+
+The socket path lives under =temporary-file-directory= (=/tmp/= on
+Linux/macOS, =%TEMP%= on Windows) and includes the effective UID and Emacs
+process id, e.g. =cj-music-mpv-1000-12345.sock=. On startup, remove stale
+=cj-music-mpv-*= sockets for the current UID when no matching process owns
+them. On Emacs exit, stop playback and remove the active socket.
+
+Minimum mpv version: 0.17 (when JSON IPC stabilized). All current
+Linux/macOS/Windows distributions ship something newer.
+
+Backend responsibilities:
+
+- Start mpv for the selected track and connect to the IPC socket with
+ =make-network-process=.
+- Send JSON commands for pause/resume, seek, and volume.
+- Subscribe to mpv events such as =playback-restart=, =pause=, and =end-file=.
+- Query metadata on track start with =get_property metadata= and update the
+ selected track's metadata slots.
+- Distinguish deliberate stops from natural track completion so the sentinel
+ does not auto-advance after an explicit stop.
+- Report process and IPC errors through =cj/music-state-change-functions= with
+ =:event error=.
+
+** Platform Support
+
+Linux and macOS are the primary v1 targets. Both expose mpv's JSON IPC over
+a Unix domain socket, which Emacs reads with =make-network-process :family
+'local=. All features (play/stop/next/previous, pause/resume, seek, volume,
+metadata) work identically on those platforms.
+
+Windows is best-effort. mpv on Windows uses named pipes
+(=\\.\pipe\<name>=) for IPC instead of Unix sockets, and Emacs's
+=make-network-process= does not natively connect to Windows named pipes.
+Rather than block v1 on a Windows IPC layer, v1 ships a degraded mode on
+Windows:
+
+- Spawn mpv with =start-process= and feed commands over stdin or via
+ per-command =call-process= invocations.
+- Available commands: play, stop, next, previous.
+- Not available on Windows in v1: pause/resume, seek, volume.
+- =M-x cj/music-doctor= reports the degraded state on Windows so the user
+ is not surprised by missing functionality.
+
+Craig's call, 2026-05-15: best-effort on Windows is acceptable for v1.
+Anyone who needs full Windows parity can fund a follow-up that wires named
+pipes via =mpvc.exe= shellout or a =w32-*= named-pipe client. This call
+unblocks the implementer to focus on the Linux/macOS path without spending
+v1 budget on Windows IPC plumbing.
+
+** Selected-Track Representation
+
+Use =selected-index= as the durable state value. The playlist buffer should
+display the selected/current track with an overlay plus a selected-track face.
+
+Reordering should:
+
+1. Swap entries in the playlist state's =tracks= vector/list.
+2. Update =selected-index= so it continues to point at the same logical track.
+3. Re-render the playlist buffer.
+4. Reposition the selected overlay.
+
+Consume mode should remove the finished track from playlist state, then
+re-render. It should not edit raw buffer text as the source of truth.
+
+** Playlist Buffer
+
+Define a package-owned major mode, for example =cj/music-playlist-mode=.
+
+The buffer should preserve the current key surface:
+
+- =RET= or =p= to play the selected track.
+- =SPC= to pause/resume.
+- =s= to stop.
+- =>= / =n= and =<= / =P= for next/previous.
+- =f= / =b= for seek forward/backward.
+- =+= / === / =-= for volume.
+- =a= to add music.
+- =A= to append the track at point to an M3U.
+- =c= / =C= to clear.
+- =L=, =S=, =E=, and =g= for playlist load/save/edit/reload.
+- =r=, =t=, =z=, and =x= for repeat playlist, repeat track, random, and
+ consume.
+- =Z= to shuffle.
+- =i= for track info.
+- =o= to jump to the playing track.
+- =q= to bury the playlist buffer.
+- =S-<up>= / =S-<down>= and =C-<up>= / =C-<down>= for reordering.
+
+The current overlay/header design can stay, but it should read from the
+package state APIs instead of EMMS variables.
+
+The active-window background highlight should be preserved, because it is part
+of the current playlist window affordance.
+
+** Playlist State Rules
+
+- Shuffle changes playlist order and clears random history.
+- Reload replaces playlist tracks from disk and clears random history.
+- Save writes only track names/URLs to M3U; random history and mode state are
+ not persisted.
+- Repeat playlist, repeat track, random, and consume are package-owned runtime
+ flags.
+- Clearing a playlist clears the associated M3U file path.
+
+** M3U Handling
+
+Keep M3U as the persistence format. Existing helpers should be reused or moved
+behind pure APIs:
+
+- =cj/music--m3u-file-tracks= should parse paths and URLs.
+- Saving should write one track per line, using relative paths where possible.
+- Radio station creation should keep writing =#EXTM3U= and =#EXTINF= entries.
+
+** Test Architecture
+
+The rewrite should not update every command-flow test with one-off mocks.
+Introduce a shared fake backend first:
+
+#+begin_src emacs-lisp
+;; tests/testutil-music-backend.el
+(defvar cj/test-music-fake-backend
+ '(:name fake
+ :available-p cj/test-music--fake-available-p
+ :play cj/test-music--fake-play
+ :pause cj/test-music--fake-pause
+ :resume cj/test-music--fake-resume
+ :stop cj/test-music--fake-stop
+ :seek cj/test-music--fake-seek
+ :volume cj/test-music--fake-volume
+ :status cj/test-music--fake-status
+ :metadata cj/test-music--fake-metadata))
+#+end_src
+
+The fake backend should keep a simple event ledger, for example
+=(:playing-p BOOL :paused-p BOOL :track TRACK :events LIST)=. Command tests
+bind =cj/music-current-backend= to this fake and assert ordered backend events
+instead of stubbing individual EMMS functions.
+
+Before rewriting non-pure command implementations, add or preserve
+characterization tests for:
+
+- =cj/music-next=
+- =cj/music-previous=
+- =cj/music-toggle-consume=
+- =cj/music-playlist-toggle=
+- =cj/music-playlist-load=
+- =cj/music-playlist-clear=
+
+The existing pure-helper tests should mostly survive unchanged. The command
+tests, random-navigation tests, consume tests, playlist-buffer tests, and header
+tests should migrate to package state plus the fake backend.
+
+The real mpv IPC client should have integration tests tagged =:slow= and
+skipped when =mpv= is not on =PATH=. Default =make test= should not depend on
+mpv being installed.
+
+** Performance Budget
+
+V1 should keep the UI responsive for realistic playlist sizes:
+
+- =cj/music-playlist-load= on a 1000-track M3U should complete in under 500 ms,
+ excluding disk cold-cache effects.
+- =S-<up>= and =S-<down>= should return control within 50 ms for playlists up
+ to 5000 tracks.
+- Pause/resume command dispatch over mpv IPC should complete in under 100 ms,
+ excluding audio-device resume latency.
+- Header refresh after metadata arrival should stay below a frame budget
+ target of 16 ms.
+
+Full playlist re-rendering is acceptable for load, clear, shuffle, reload, and
+consume-after-finish. Reordering should avoid an obvious O(n) full erase and
+insert on every keypress if it misses the 50 ms budget. Start simple, measure,
+then add incremental line swaps or rendered-line caching only if needed.
+
+Metadata extraction must be lazy. Query mpv metadata when a track starts and
+refresh the header when it arrives. Do not eagerly scan all tracks on playlist
+load.
+
+** Parity Walk
+
+Before removing the EMMS implementation, run a manual parity walk against the
+new implementation:
+
+1. =F10= opens the playlist in a bottom side window; =F10= again closes it.
+2. =C-; m a= completes files and dirs under =cj/music-root=; choosing a file
+ adds that track.
+3. Choosing a directory adds music files from that tree.
+4. In Dirvish, marking files and pressing =+= adds them.
+5. In the playlist, =RET= plays, =SPC= pauses, and =SPC= resumes.
+6. =>= advances and =<= goes back.
+7. =z= toggles random; next chooses randomly; previous uses random history.
+8. =r= toggles repeat-playlist.
+9. =t= toggles repeat-track.
+10. =x= toggles consume; finished tracks disappear.
+11. =S= saves an M3U in =cj/music-m3u-root=.
+12. =L= loads a saved playlist in order.
+13. =g= reloads the current M3U after manual edits.
+14. =E= opens the M3U for editing.
+15. =R= creates a radio-station M3U that can be loaded and played.
+16. =S-<up>= and =S-<down>= reorder tracks while state and view stay in sync.
+17. =c= and =C= clear the playlist.
+18. =q= buries the playlist buffer.
+19. =i= shows current track info.
+20. =o= centers the playlist on the current track.
+21. =+= and =-= adjust volume and the change persists across track changes.
+22. =f= and =b= seek forward/backward.
+
+* Migration Plan
+
+1. Extract pure helpers and tests into EMMS-free units: file validation,
+ recursive collection, M3U parsing/writing, safe filenames, radio station
+ content, URL/file track typing, and playlist state operations.
+2. Introduce package-owned track and playlist state structs.
+3. Add =cj/music-playlist-mode= and make it render package playlist state with
+ selected-track overlay support.
+4. Add =tests/testutil-music-backend.el= and migrate command-flow tests to the
+ fake backend.
+5. Implement the mpv backend in focused steps:
+ - Process spawn, socket path management, IPC connection, and state-change
+ hook plumbing.
+ - Play, stop, next, and previous, including finished-track auto-advance.
+ - Pause/resume, seek, and volume through IPC.
+ - Metadata read on track start through IPC.
+6. Rewire public commands and Dired/Dirvish integration to use the new
+ state/backend APIs.
+7. Replace EMMS functions in =cj/music-map= and the playlist-mode keymap with
+ =cj/music-*= commands.
+8. Remove =cj/emms--setup= and the on-demand EMMS loading pattern.
+9. Delete the =use-package emms= block once parity is covered.
+
+No EMMS compatibility adapter is planned. This is a personal config, and the
+cleaner migration is to keep existing public =cj/music-*= command names while
+swapping their implementation behind the scenes.
+
+* Acceptance Criteria
+
+- Loading =music-config.el= does not require EMMS or reference EMMS symbols.
+- =init.el= still loads after the =use-package emms= block is removed.
+- A new smoke test confirms =music-config.el= can be required in batch with no
+ EMMS package installed.
+- Existing focused music tests pass without EMMS preload or EMMS stubs.
+- =tests/testutil-music-backend.el= exists and command-flow tests use it
+ instead of direct EMMS stubs.
+- New tests cover playlist state, backend command dispatch, IPC command
+ formatting, M3U persistence, Dired/Dirvish add routing, and the EMMS-free load
+ smoke path.
+- Slow mpv IPC integration tests are tagged =:slow= and skipped when =mpv= is
+ unavailable.
+- The F10 and =C-; m= workflows still open/show the playlist and expose the
+ same high-level commands.
+- All keys from the current playlist-mode keymap work in
+ =cj/music-playlist-mode=.
+- M3U load/save/reload/edit and radio station creation work without EMMS.
+- Local-file and stream URL playback work through mpv.
+- Pause/resume, seek, and volume work through mpv IPC.
+- Random, repeat playlist, repeat track, consume, shuffle, and track reordering
+ are represented in package-owned state and covered by focused tests.
+- The parity walk passes.
+- The performance budget is met or deviations are documented with measurements.
+
+* Risks
+
+| Risk | Mitigation |
+|-----------------------------------------------+-------------------------------------------------------------------------------------------------------------|
+| mpv IPC socket races or stale sockets | Use UID/PID-stamped socket paths, clean stale sockets on startup, and remove the active socket on exit. |
+| Auto-advance fires after explicit stop | Track deliberate stop state and test sentinel/event ordering. |
+| Metadata availability differs by stream/file | Treat metadata as optional; filename/URL descriptions remain the fallback. |
+| Playlist buffer gets out of sync with state | Make state the only source of truth and render buffer text from state after every playlist mutation. |
+| Dirvish =+= workflow regresses | Include Dired/Dirvish add routing in migration and tests. |
+| Test rewrite spreads bespoke fakes everywhere | Add one shared fake backend and migrate command tests through it. |
+| Playlist rendering is too slow on large M3Us | Start simple, measure against the budget, and add incremental rendering only if needed. |
+| Rewrite is too broad for one commit | Split implementation by the migration plan; keep pure helpers and state changes separate from backend work. |
+
+* Considered But Not Chosen
+
+** Publish as a standalone MELPA package in v1
+
+Rejected for v1. A public package would require namespace cleanup,
+=Package-Requires=, autoload cookies, defgroups/defcustoms, license headers,
+README/CHANGELOG work, package-lint/checkdoc cleanup, CI matrix support, and
+removal of personal config dependencies like =user-constants= and
+=cj/custom-keymap=. That work is real and useful only after the personal
+module migration proves the design. The v1 scope stays in
+=modules/music-config.el=.
+
+** Rewrite every test individually
+
+Rejected. Roughly half of the current music tests exercise EMMS-backed command
+flows. Replacing each EMMS mock one at a time would duplicate setup and make
+backend semantics inconsistent across tests. A shared fake backend gives the
+rewrite one seam to test command dispatch, state changes, and event ordering.
+
+** Eager metadata extraction for every playlist track
+
+Rejected. Scanning metadata for every track on load would require either many
+short mpv invocations or another tag-reading dependency, and it scales poorly
+for large M3Us. V1 reads metadata lazily from mpv IPC only when a track starts.
+
+** Full incremental playlist renderer from the start
+
+Deferred. Incremental rendering is more complex and may not be needed for the
+actual playlist sizes in this config. The spec sets a budget instead: full
+rendering is fine where it meets the budget, and reorder operations should only
+gain incremental swaps or cached rendered lines if measurement shows a problem.
+
+** Depend on an external mpv IPC package immediately
+
+Undecided, not chosen as a requirement. A local JSON IPC helper may be small
+enough and easier to test in this config. Depending on an existing package is
+still open if the local helper starts to recreate too much protocol machinery.
+
+** Add =webm= and =ape= to =cj/music-file-extensions=
+
+Rejected. =~/music= contains a small number of =webm= and =ape= files (~22
+each), and mpv can play both, but Craig's call (2026-05-15) is to leave the
+extension list as-is. Those files stay silently filtered out of fuzzy
+completion and recursive directory adds. Easy to revisit later by adding the
+two strings to the list; no architectural impact.
+
+* Open Decisions
+
+- Exact mpv IPC client implementation: small local JSON helper or dependency on
+ an existing mpv IPC package.
+- Whether metadata should be cached only in memory or written into extended M3U
+ comments later.