| 1 | ;;; emms-volume-mpv.el --- Volume function to adjust mpv volume easily -*- lexical-binding: t; -*- |
| 2 | |
| 3 | ;; Copyright (C) 2025 Free Software Foundation, Inc. |
| 4 | |
| 5 | ;; Author: Richard Sent <richard@freakingpenguin.com> |
| 6 | |
| 7 | ;; This file is part of EMMS. |
| 8 | |
| 9 | ;; EMMS is free software; you can redistribute it and/or modify |
| 10 | ;; it under the terms of the GNU General Public License as published by |
| 11 | ;; the Free Software Foundation; either version 3, or (at your option) |
| 12 | ;; any later version. |
| 13 | ;; |
| 14 | ;; EMMS is distributed in the hope that it will be useful, |
| 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 17 | ;; GNU General Public License for more details. |
| 18 | ;; |
| 19 | ;; You should have received a copy of the GNU General Public License |
| 20 | ;; along with EMMS; see the file COPYING. If not, write to the |
| 21 | ;; Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, |
| 22 | ;; Boston, MA 02110-1301, USA. |
| 23 | |
| 24 | ;;; Commentary: |
| 25 | ;; |
| 26 | ;; This file defines a function to raise or lower the volume of mpv. |
| 27 | ;; It can be used stand-alone by passing a process object, though it |
| 28 | ;; is meant for usage with Emms, particularly with emms-volume.el and |
| 29 | ;; emms-player-mpv.el. |
| 30 | ;; |
| 31 | ;; To use add the following to your Emms configuration |
| 32 | ;; (setq emms-volume-change-function 'emms-volume-mpv-change) |
| 33 | |
| 34 | ;;; History: |
| 35 | |
| 36 | ;; January 2025: First release, partly based on emms-volume-pulse.el. |
| 37 | |
| 38 | ;;; Code: |
| 39 | |
| 40 | (require 'emms-player-mpv) |
| 41 | |
| 42 | (defcustom emms-volume-mpv-method 'native |
| 43 | "How Emms should attempt to adjust mpv's volume. |
| 44 | |
| 45 | If `native', Emms will adjust mpv's volume property. This |
| 46 | provides the same experience as adjusting the volume slider in |
| 47 | mpv. |
| 48 | |
| 49 | If `system', Emms will adjust mpv's ao-volume property, which |
| 50 | adjusts the volume using the system audio service, such as |
| 51 | Pulseaudio. Depending on what audio service is being used this |
| 52 | may either change the mpv application's volume or global volume. |
| 53 | |
| 54 | If `smart', Emms will adjust both mpv's volume and ao-volume |
| 55 | properties. When raising volume, the native volume will be raised |
| 56 | to 100. Emms will then switch to adjusting system volume to 100 |
| 57 | before raising the native volume again. When lowering volume, |
| 58 | Emms will lower the software volume to 100, then lower system |
| 59 | volume to 0. |
| 60 | |
| 61 | Both `system' and `smart' require mpv to expose the ao-volume |
| 62 | property. This property is only available while mpv audio output |
| 63 | is active. If audio output is not active, the volume will not be |
| 64 | changed. |
| 65 | |
| 66 | Additionally, the percentage provided by and set for ao-volume |
| 67 | and thus this module may not match what is reported by the system |
| 68 | audio program." |
| 69 | :type '(choice (const :tag "MPV Volume" native) |
| 70 | (const :tag "System Volume" system) |
| 71 | (const :tag "Smart" smart)) |
| 72 | :group 'emms-volume) |
| 73 | |
| 74 | (defvar emms-volume-mpv--volume-sync (make-mutex "emms-volume-mpv--volume-sync") |
| 75 | "Ensure only one volume-change function runs to completion at a |
| 76 | time.") |
| 77 | |
| 78 | (defun emms-volume-mpv-synchronous-ipc (cmd &optional proc) |
| 79 | "Run mpv command and get result synchronously for current thread. |
| 80 | |
| 81 | This must not be run by the main thread. The handler for |
| 82 | emms-player-mpv-ipc-req-send runs in the main thread, potentially |
| 83 | causing a deadlock." |
| 84 | (when (eq main-thread (current-thread)) |
| 85 | (error "This function cannot be invoked by the main thread")) |
| 86 | (let* ((emms-volume-mpv--ipc-sync (make-mutex "emms-volume-mpv--ipc-sync")) |
| 87 | (emms-volume-mpv--ipc-sync-check (make-condition-variable emms-volume-mpv--ipc-sync |
| 88 | "emms-volume-mpv--ipc-sync-check")) |
| 89 | (emms-volume-mpv--ipc-sync-reply nil)) |
| 90 | (with-mutex emms-volume-mpv--ipc-sync |
| 91 | (emms-player-mpv-ipc-req-send |
| 92 | cmd |
| 93 | #'(lambda (data err) |
| 94 | (with-mutex emms-volume-mpv--ipc-sync |
| 95 | (setq emms-volume-mpv--ipc-sync-reply (list data err)) |
| 96 | (condition-notify emms-volume-mpv--ipc-sync-check))) |
| 97 | proc) |
| 98 | (while (not emms-volume-mpv--ipc-sync-reply) |
| 99 | (condition-wait emms-volume-mpv--ipc-sync-check)) |
| 100 | (cl-multiple-value-bind (data err) emms-volume-mpv--ipc-sync-reply |
| 101 | (if err (error "Failed to run %s, %s" cmd err) data))))) |
| 102 | |
| 103 | (defun emms-volume-mpv-limit (vol volume-max &optional volume-min) |
| 104 | "Limit VOL to the range [0 - volume-max]." |
| 105 | (max (min vol volume-max) (or volume-min 0))) |
| 106 | |
| 107 | (defun emms-volume-mpv--smart-increment (native-old system-old amount native-max) |
| 108 | (cond |
| 109 | ((< native-old 100) |
| 110 | (list (emms-volume-mpv-limit (+ native-old amount) 100) system-old)) |
| 111 | ((< system-old 100) |
| 112 | (list native-old (emms-volume-mpv-limit (+ system-old amount) 100))) |
| 113 | (t (list (emms-volume-mpv-limit (+ native-old amount) native-max) system-old)))) |
| 114 | |
| 115 | (defun emms-volume-mpv--smart-decrement (native-old system-old amount native-max) |
| 116 | (cond |
| 117 | ((> native-old 100) |
| 118 | (list (emms-volume-mpv-limit (+ native-old amount) native-max 100) system-old)) |
| 119 | (t (list native-old (emms-volume-mpv-limit (+ system-old amount) 100))))) |
| 120 | |
| 121 | (defun emms-volume-mpv--smart-change (native-old system-old amount native-max) |
| 122 | (if (>= amount 0) |
| 123 | (emms-volume-mpv--smart-increment native-old system-old amount native-max) |
| 124 | (emms-volume-mpv--smart-decrement native-old system-old amount native-max))) |
| 125 | |
| 126 | ;;;###autoload |
| 127 | (defun emms-volume-mpv-change (amount &optional proc) |
| 128 | "Change volume by AMOUNT using mpv process PROC." |
| 129 | (unless (or emms-player-mpv-ipc-proc proc) |
| 130 | (error "mpv is not currently running")) |
| 131 | ;; mpv does not protect against storing volumes > volume-max. We |
| 132 | ;; must retrieve volume-max and verify the target volume. |
| 133 | (make-thread |
| 134 | (lambda () |
| 135 | (with-mutex emms-volume-mpv--volume-sync |
| 136 | (with-demoted-errors "Failed to adjust the volume: %s" |
| 137 | (let* ((native-max (emms-volume-mpv-synchronous-ipc '(get_property volume-max) proc)) |
| 138 | (native-old (emms-volume-mpv-synchronous-ipc '(get_property volume) proc)) |
| 139 | (system-old (emms-volume-mpv-synchronous-ipc '(get_property ao-volume) proc))) |
| 140 | (pcase emms-volume-mpv-method |
| 141 | ('native (let ((volume (emms-volume-mpv-limit (+ native-old amount) native-max))) |
| 142 | (emms-volume-mpv-synchronous-ipc |
| 143 | `(set_property volume ,volume) proc) |
| 144 | (message "Native volume is %d%%" volume))) |
| 145 | ('system (let ((volume (emms-volume-mpv-limit (+ system-old amount) 100))) |
| 146 | (emms-volume-mpv-synchronous-ipc |
| 147 | `(set_property ao-volume ,volume) proc) |
| 148 | (message "System volume is %d%%" volume))) |
| 149 | ('smart (cl-multiple-value-bind (native system) |
| 150 | (emms-volume-mpv--smart-change native-old system-old |
| 151 | amount native-max) |
| 152 | (emms-volume-mpv-synchronous-ipc |
| 153 | `(set_property volume ,native) proc) |
| 154 | (emms-volume-mpv-synchronous-ipc |
| 155 | `(set_property ao-volume ,system) proc) |
| 156 | (message "Native volume is %d%% and system volume is %d%%" |
| 157 | native system)))))))))) |
| 158 | |
| 159 | (provide 'emms-volume-mpv) |
| 160 | |
| 161 | ;;; emms-volume-mpv.el ends here |