diff options
Diffstat (limited to 'docs/design/music-config-without-emms.org')
| -rw-r--r-- | docs/design/music-config-without-emms.org | 543 |
1 files changed, 0 insertions, 543 deletions
diff --git a/docs/design/music-config-without-emms.org b/docs/design/music-config-without-emms.org deleted file mode 100644 index 929423df6..000000000 --- a/docs/design/music-config-without-emms.org +++ /dev/null @@ -1,543 +0,0 @@ -#+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. |
