aboutsummaryrefslogtreecommitdiff
path: root/docs/design/music-config-without-emms.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/music-config-without-emms.org')
-rw-r--r--docs/design/music-config-without-emms.org543
1 files changed, 543 insertions, 0 deletions
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.