aboutsummaryrefslogtreecommitdiff
path: root/modules/music-config.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/music-config.el')
-rw-r--r--modules/music-config.el207
1 files changed, 108 insertions, 99 deletions
diff --git a/modules/music-config.el b/modules/music-config.el
index 76fff283b..86f6eb130 100644
--- a/modules/music-config.el
+++ b/modules/music-config.el
@@ -5,90 +5,18 @@
;; Layer: 4 (Optional).
;; Category: O/D/P/S.
;; Load shape: eager.
-;; Eager reason: none; optional music workflow that registers a music keymap, a
-;; command-loaded deferral candidate. EMMS hooks should run only after EMMS.
-;; Top-level side effects: defines a music keymap under cj/custom-keymap, one
-;; global key, package config.
+;; Eager reason: none; optional music workflow that registers a music keymap.
+;; Top-level side effects: defines C-; m map, one global key, package config.
;; Runtime requires: subr-x, user-constants, keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; Music management in Emacs via EMMS with MPV backend.
-;; Focus: simple, modular helpers; consistent error handling; streamlined UX.
-;;
-;; Highlights:
-;; - Fuzzy add: select files/dirs; dirs have trailing /; case-insensitive; stable order
-;; - Recursive directory add
-;; - Dired/Dirvish integration (add selection)
-;; - M3U playlist save/load/edit/reload
-;; - Radio station M3U creation (streaming URLs supported)
-;; - Playlist window toggling
-;; - Consume mode (remove tracks after playback)
-;; - MPV as player (no daemon required)
-;;
-;; Keybindings (playlist-mode-map):
-;;
-;; Aligned with ncmpcpp defaults where possible (83% match).
-;; Additional EMMS-specific bindings for features ncmpcpp lacks.
-;;
-;; Key Action ncmpcpp default Match
-;; ─── ────── ─────────────── ─────
-;; Playback
-;; SPC pause add_item *
-;; s stop stop ✓
-;; > / n next track next ✓
-;; < / P previous track previous ✓
-;; p play selected (enter) ✓
-;; f seek forward seek_forward ✓
-;; b seek backward seek_backward ✓
-;;
-;; Toggles
-;; r repeat playlist toggle_repeat ✓
-;; t repeat track (none) +
-;; z random toggle_random ✓
-;; x consume toggle_crossfade *
-;; Z shuffle shuffle ✓
-;;
-;; Volume
-;; + / = volume up volume_up ✓
-;; - volume down volume_down ✓
-;;
-;; Info
-;; i song info show_song_info ✓
-;; o jump to playing jump_to_playing ✓
-;;
-;; Playlist management
-;; a add music (fuzzy) add_selected ✓
-;; c / C clear playlist clear_playlist ✓
-;; S save playlist (none) +
-;; L load playlist (none) +
-;; E edit playlist M3U (none) +
-;; g reload playlist (none) +
-;; A append track to M3U (none) +
-;; q quit/bury quit ✓
-;;
-;; Track reordering
-;; S-up move track up (shift-up) ✓
-;; S-down move track down (shift-down) ✓
-;; C-up move track up (alias) (none) +
-;; C-down move track down (alias) (none) +
-;;
-;; Other
-;; R create radio station (none) +
-;;
-;; Legend: ✓ = matches ncmpcpp default
-;; * = intentional divergence (see below)
-;; + = EMMS-only feature
-;;
-;; Intentional divergences from ncmpcpp defaults:
-;;
-;; SPC/p swap: ncmpcpp defaults p=pause, SPC=add_item_to_playlist.
-;; This config uses SPC=pause (more natural in Emacs) and p=play
-;; selected track. Pause via SPC is a common media player convention.
-;;
-;; x=consume vs crossfade: ncmpcpp's crossfade is an mpd daemon
-;; feature. EMMS uses mpv directly, so consume mode (remove tracks
-;; after playback) is more useful here.
+;; EMMS setup using an mpv subprocess player, M3U playlist helpers, fuzzy
+;; file/directory adds, Dired/Dirvish integration, radio-station creation, and
+;; playlist window toggling.
;;
+;; The playlist keymap intentionally follows ncmpcpp where it maps cleanly, with
+;; EMMS-specific additions for M3U editing and consume mode.
+
;;; Code:
(require 'subr-x)
@@ -108,8 +36,6 @@
(defvar emms-random-playlist)
(defvar emms-playlist-selected-marker)
(defvar emms-source-file-default-directory)
-(defvar emms-player-mpv-parameters)
-(defvar emms-player-mpv-regexp)
(defvar emms-player-playing-p)
(defvar emms-player-paused-p)
(defvar emms-playlist-mode-map)
@@ -146,9 +72,98 @@
(defvar cj/music-file-extensions '("aac" "flac" "m4a" "mp3" "ogg" "opus" "wav")
"List of valid music file extensions.")
+(defvar cj/music-seek-seconds 5
+ "Seconds to move when seeking forward or backward in the current track.")
+
(defvar cj/music-playlist-buffer-name "*EMMS-Playlist*"
"Name of the EMMS playlist buffer used by this configuration.")
+;;; Subprocess mpv player (reliable playback)
+
+;; The IPC player (emms-player-mpv) drives mpv over a socket -- start mpv idle,
+;; connect, send loadfile. That handshake was leaving mpv loaded but never
+;; streaming, so playback silently failed. Driving mpv with the track as a
+;; direct argument -- the invocation that plays every time -- is the reliable
+;; path. --no-config isolates this mpv from the interactive/video mpv setup so
+;; the two cannot interfere. Pause is in place via process signals; in-track
+;; seek is not available with a subprocess player (the trade for reliability).
+
+(declare-function emms-player "emms")
+(declare-function emms-player-set "emms")
+(declare-function emms-player-simple-start "emms-player-simple")
+(declare-function emms-player-simple-stop "emms-player-simple")
+(defvar emms-player-simple-process-name)
+(defvar emms-player-cj/music-mpv)
+
+(defvar cj/music--mpv-regex
+ (concat "\\(?:\\." (regexp-opt cj/music-file-extensions) "\\'\\)"
+ "\\|\\`\\(?:https?\\|mms\\)://")
+ "Track names the subprocess mpv player handles: music files or stream URLs.")
+
+(defvar cj/music--mpv-socket
+ (expand-file-name "emms/mpv-control.sock" user-emacs-directory)
+ "IPC control socket for the subprocess mpv player.
+mpv opens it per playback via --input-ipc-server. It does NOT affect startup:
+mpv still plays the track passed as a direct argument, so the reliable start is
+unchanged. The socket only carries control commands (seek) to the already
+playing process, which is where the old idle + loadfile handshake failed.")
+
+(defun cj/music--mpv-start (track)
+ "Play TRACK by running mpv with the track name as a direct argument."
+ (emms-player-simple-start (emms-track-name track)
+ 'emms-player-cj/music-mpv
+ "mpv"
+ (list "--no-video" "--no-config" "--really-quiet"
+ (concat "--input-ipc-server=" cj/music--mpv-socket))))
+
+(defun cj/music--mpv-stop ()
+ "Stop the mpv subprocess."
+ (emms-player-simple-stop))
+
+(defun cj/music--mpv-playable-p (track)
+ "Return non-nil if the subprocess mpv player can play TRACK."
+ (and (executable-find "mpv")
+ (memq (emms-track-type track) '(file url))
+ (string-match cj/music--mpv-regex (emms-track-name track))))
+
+(defun cj/music--mpv-pause ()
+ "Pause the mpv subprocess in place by stopping it (SIGSTOP)."
+ (let ((proc (get-process emms-player-simple-process-name)))
+ (when (and proc (process-live-p proc))
+ (signal-process proc 'SIGSTOP))))
+
+(defun cj/music--mpv-resume ()
+ "Resume the paused mpv subprocess (SIGCONT)."
+ (let ((proc (get-process emms-player-simple-process-name)))
+ (when (and proc (process-live-p proc))
+ (signal-process proc 'SIGCONT))))
+
+(defun cj/music--mpv-command (json)
+ "Send JSON (a one-line mpv IPC command) to the control socket.
+A no-op when nothing is playing or the socket is gone, so it never errors."
+ (when (file-exists-p cj/music--mpv-socket)
+ (ignore-errors
+ (let ((proc (make-network-process :name "cj-music-mpv-cmd"
+ :family 'local
+ :service cj/music--mpv-socket
+ :noquery t)))
+ (unwind-protect
+ (progn (process-send-string proc (concat json "\n"))
+ (accept-process-output proc 0.1))
+ (delete-process proc))))))
+
+(defun cj/music-seek-forward ()
+ "Seek `cj/music-seek-seconds' seconds forward in the current track."
+ (interactive)
+ (cj/music--mpv-command
+ (format "{\"command\": [\"seek\", %d, \"relative\"]}" cj/music-seek-seconds)))
+
+(defun cj/music-seek-backward ()
+ "Seek `cj/music-seek-seconds' seconds backward in the current track."
+ (interactive)
+ (cj/music--mpv-command
+ (format "{\"command\": [\"seek\", %d, \"relative\"]}" (- cj/music-seek-seconds))))
+
;;; Buffer-local state
(defvar-local cj/music-playlist-file nil
@@ -843,7 +858,7 @@ For URL tracks: decoded URL."
:commands (emms-mode-line-mode)
:config
(require 'emms-setup)
- (require 'emms-player-mpv)
+ (require 'emms-player-simple)
(require 'emms-playlist-mode)
(require 'emms-source-file)
(require 'emms-source-playlist)
@@ -852,8 +867,13 @@ For URL tracks: decoded URL."
(setq emms-source-file-default-directory cj/music-root)
(setq emms-playlist-default-major-mode 'emms-playlist-mode)
- ;; Use MPV as player - MUST be set before emms-all
- (setq emms-player-list '(emms-player-mpv))
+ ;; Use the reliable subprocess mpv player (built above) - MUST be set before emms-all
+ (setq emms-player-cj/music-mpv
+ (emms-player #'cj/music--mpv-start #'cj/music--mpv-stop
+ #'cj/music--mpv-playable-p))
+ (emms-player-set emms-player-cj/music-mpv 'pause #'cj/music--mpv-pause)
+ (emms-player-set emms-player-cj/music-mpv 'resume #'cj/music--mpv-resume)
+ (setq emms-player-list '(emms-player-cj/music-mpv))
;; Now initialize EMMS
(emms-all)
@@ -862,17 +882,6 @@ For URL tracks: decoded URL."
(emms-playing-time-display-mode -1)
(emms-mode-line-mode -1)
- ;; MPV configuration
- ;; MPV supports both local files and stream URLs
- (setq emms-player-mpv-parameters
- '("--quiet" "--no-video" "--audio-display=no"))
-
- ;; Update supported file types for mpv player
- (setq emms-player-mpv-regexp
- (concat "\\(?:\\`\\(?:https?\\|mms\\)://\\)\\|\\(?:\\."
- (regexp-opt cj/music-file-extensions)
- "\\'\\)"))
-
;; Keep cj/music-playlist-file in sync if playlist is cleared.
;; Ensure we don't stack duplicate advice on reload.
(advice-remove 'emms-playlist-clear #'cj/music--after-playlist-clear)
@@ -908,8 +917,8 @@ For URL tracks: decoded URL."
(">" . cj/music-next)
("P" . cj/music-previous)
("<" . cj/music-previous)
- ("f" . emms-seek-forward)
- ("b" . emms-seek-backward)
+ ("f" . cj/music-seek-forward)
+ ("b" . cj/music-seek-backward)
("q" . emms-playlist-mode-bury-buffer)
("a" . cj/music-fuzzy-select-and-add)
;; Toggles (aligned with ncmpcpp)