[Emacs] EMMS: 媒体播放器
Table of Contents
简介
EMMS 全称 Emacs Multimedia System ,即 Emacs 的多媒体系统,用于在 Emacs 中展示和播放多媒体 [1] ,可以接入多种后端。我把它用来做音乐播放器,离入魔又近了一步。
EMMS 有非常详细的文档,用于参阅检索:《The Emms Manual》。
安装
可以直接用 package.el
的 M-x package-install RET emms
来安装,也可以下载到本地,通过 (add-to-list 'load-path "/path/to/emms")
来导入。
EMMS 的各个模块的功能拆成了许多小型的库,自定义程度高的话可通过一个个库的导入来灵活安排自己的配置。对于懒得折腾的用户, EMMS 提供了一步导入的配置。
快速配置
EMMS 在库 emms-setup
给出了标准配置,可以通过它,直接使用标准配置。
(require 'emms-setup)
(emms-minimalistic)
;; or
(emms-all)
;; (emms-default-players)
emms-minimalistic
导入最小配置,emms-all
导入标准配置,二者选一个即可。emms-default-player
用于指明默认后端,最初的状态是没有指明的。emms-minimalistic
和emms-all
都调用了这个函数来初始化可选后端。
自定义配置
我最后选择的是使用自定义的、模块化的配置,顺便学习一下 EMMS 的相关内容。
后端选择
我的系统是 Arch Linux ,平时用的多媒体播放器是 mpv -–— 之前一直是用来播放视频的,它当然也能播放音乐。调用 EMMS 封装的 mpv 相关接口 [1] :
(require 'emms-player-mpv)
(setq-default emms-player-list '(emms-player-mpv)
emms-player-mpv-environment '("PULSE_PROP_media.role=music")
emms-player-mpv-parameters '("--quiet" "--really-quiet"
"--no-audio-display" "--force-window=no" "--vo=null"))
EMMS 支持的后端蛮多的,有 ogg123, mpg321, mpv, vlc 等,其余后端可以去参考手册的配置。
本地音乐路径
我转用本地音乐的原因就是稳定。之前尝试过的音乐软件有:
- YesPlayMusic, 高颜值的第三方网易云音乐客户端
- Listen 1, 整合多平台的音乐客户端
- FeelUOwn, Python 写的音乐客户端,比较生猛,把各个平台以及各种功能分为插件,折腾起来还不错
用的时候都是实时去各大音乐平台的接口请求,然后由于各种版权和会员的原因,很多音乐都无法播放或者返回的资源是一些听感不妥的 Live 版本,最终决定把音乐保存到本地。之前一直没落实是怕占空间,结果弄完发现才 2G 左右,所以归根结底还是懒……
既然我都用本地音乐了,为什么不用更简单的播放器呢?所以就尝试了使用 EMMS 的想法 – 虽然配置以及下载花了一天的时间。
下面是关于本地音乐路径寻找的配置:
(require 'emms-source-file)
(setq-default emms-source-file-default-directory "~/Music/")
有一个可选变量 emms-source-file-directory-tree-function
, 这个变量指定了读取本地音乐的函数,它的默认值是 emms-source-file-directory-tree-internal
, 有个备选 emms-source-file-directory-tree-find
,前者速度慢一些,后者速度更快,但是需要有 GNU 的 find
命令。
你也可以自定义这样一个函数,然后通过 (setq-default emms-source-file-directory-tree-function your-func)
配置。它传入两个参数,第一个是 dir
,即扫描的目录;第二个是 regex
,一个正则匹配样式,扫描出的文件需要符合才会纳入。你可以通过这个来自定义一些子目录或者屏蔽一些类型的文件。
本地音乐管理
在默认目录 emms-source-file-default-directory
下,建立子目录,每个子目录代表一张歌单,每个歌单放入相应的本地音乐文件。我写了一个用于快速切换歌单的函数,依赖是 f.el, 一组与文件目录相关的 API :
(defun emms-switch-playlist ()
"Switch to a new playlist, selected from `emms-source-file-default-directory',
and start playing immediately."
(interactive)
;; 如果播放列表打开了,那么先关闭
(when (get-buffer emms-playlist-buffer-name)
(kill-buffer emms-playlist-buffer-name))
;; 添加目录下的音乐文件到播放列表
(emms-add-directory
(f-expand
;; 选择歌单
(completing-read "Switch to: "
(-map #'f-base
(f-directories
emms-source-file-default-directory
#'(lambda (dir)
;; 这里排除了 lyrics 目录
;; 它是用来存放歌词文件的
(not (member (f-base dir) '("lyrics")))))))
emms-source-file-default-directory))
;; 随机打乱播放列表
(emms-shuffle)
;; 重新打开播放列表的 buffer
(switch-to-buffer-other-window emms-playlist-buffer-name)
;; 开始播放
(emms-playlist-mode-play-smart))
播放列表
播放列表配置
播放列表本质上就是一个 buffer ,它把列表中的曲目打印到该 buffer 中,设置其为不可写并创生新的键盘映射。因此理论上说,可以打开多个播放列表,每个播放列表对应不同的歌单。不过似乎没什么必要。
(require 'emms-browser)
(setq-default
;; 使当前播放的曲目居中于播放列表
emms-playlist-mode-center-when-go t
;; 播放列表的 buffer 的 mode
emms-playlist-default-major-mode 'emms-playlist-mode
;; 播放列表的 buffer 的名字
emms-playlist-buffer-name "*Music*"
emms-track-description-function (lambda (track)
"Function for describing an EMMS track"
(let ((name (f-base (emms-track-name track))))
(seq-let (artist title) (split-string name " - ")
(format "%s - %s" title artist))))
)
emms-track-description-function
是曲目在播放列表中的显示函数,传入 track
,一个 ALIST, 装有歌曲的各种信息。它原本会把整个歌曲的本地路径输出,形如: /home/user/Music/love/Linkin Park - In the End.mp3
, 过于抽象。再按照我的习惯把歌曲名字放前面,变成: In the End - Linkin Park
.
播放列表操作
这里列举了一部分,更多的参见手册:https://www.gnu.org/software/emms/manual/#Interactive-Playlists
n
, 播放下一首p
, 播放上一首RET
, 播放光标所在行的曲目>
, 快进 10s<
, 倒退 10sd
, 将光标所在行的曲目移出播放列表P
, 暂停播放s
, 停止播放
音量设置
配置:
(require 'emms-volume)
(global-set-key (kbd "C-c +") 'emms-volume-mode-plus)
(global-set-key (kbd "C-c -") 'emms-volume-mode-minus)
;; (setq-default emms-volume-change-amount 2
;; emms-volume-mode-timeout 2)
这两个全局快捷键用于调高、降低音量。可以通过 C-c + + +
来增加 3 次音量,或 C-c - - - - -
来降低 5 次音量。以增加音量举例,它的工作过程是:先使用 C-c +
即 emms-volume-mode-plus
来增加一次音量,并临时将当前的 mode 改为一个全新的 plus-mode ;在这个模式下, +
绑定到增加音量,按了之后便可以继续增加,同时刷新当前模式的生存时间;如果不再按 +
,过了生存时间后退出这个模式。
可以通过 emms-volume-mode-timeout
来指定 plus-mode 的生存时间,默认是 2s 。
此外,可以通过设置 emms-volume-change-amount
来表明每次增加或降低的音量的百分比,默认是 2 ,也就是每次调整 2% 。
歌词配置
基本配置:
(require 'emms-lyrics)
(setq-default
;; 歌词文件存放路径
emms-lyrics-dir "~/Music/lyrics"
;; 歌词显示在 minibuffer
emms-lyrics-display-on-minibuffer t
;; 歌词不滚动
emms-lyrics-scroll-p nil)
;; 开启歌词
(emms-lyrics 1)
配置里面可以让歌词显示在 minibuffer 或状态栏,分别设置 emms-lyrics-display-on-minibuffer
/ emms-lyrics-display-on-modeline
为 t
或 nil
.
我看配置可以配置一个单独的 buffer 来显示歌词,但是我没配成功,我选择了在 minibuffer 。
歌词文件与歌曲同名(后缀不同),放在 emms-lyrics-dir
目录下即可。
歌词来源 Lyrics-fetcher
我用的是一个在论坛 [2] 里看到的前辈分享的包 lyrics-fetcher 来获取歌词,目前他可以从 genius.com 或者网易云音乐获取,我用的是后者,它更容易找到中文歌的歌词。
安装过程参见其文档。
lyrics-fetcher 根据歌曲的元数据去查歌词,因此需要保证歌曲有元数据。 lyrics-fetcher 并不可靠
曲目元数据的添加和统一
我用的是 Python 的 mutagen 库来编辑曲目的元数据。
pip install mutagen
把歌曲的保存成 歌手 - 歌名.mp3
的格式,然后执行:
import os
import mutagen
from mutagen.id3 import ID3, TIT2, TALB, TPE1, TPE2
path = os.path.expanduser("~/Music/")
for pl in os.listdir(path):
current_playlist = os.path.join(path, pl)
lst = os.listdir(current_playlist)
for each in lst:
base, ext = os.path.splitext(each)
artist, title = base.split(" - ")
artist = artist.strip()
title = title.strip()
filename = os.path.join(current_playlist, each)
print("%s" % filename)
try:
audio = ID3(filename)
except:
meta = mutagen.File(old_filename)
meta.add_tags()
meta.save()
audio = ID3(old_filename)
audio.add(TALB(encoding=1, text=title))
audio.add(TIT2(encoding=1, text=title))
audio.add(TPE1(encoding=1, text=artist))
audio.add(TPE2(encoding=1, text=artist))
audio.save()
它会遍历本地音乐目录下的子目录,即歌单中的所有音乐文件,修改其元数据,主要改的是歌名和歌手。
获取音乐元数据
EMMS 提供了几种工具的接口,参见:https://www.gnu.org/software/emms/manual/#Track-Information.
我选用的是 tinytag. 安装: pip install tinytag
.
然后是 Emacs 的配置。
(require 'emms-info-tinytag)
(setq-default
;; 同步获取音乐元信息
emms-info-asynchronously nil
;; 音乐信息获取后端后端
emms-info-functions '(emms-info-tinytag))
;; 获取音乐列表同时获取元信息
(add-to-list 'emms-track-initialize-functions #'emms-info-initialize-track)
注意的就是 emms-info-asynchronously
一定要是 nil
, 也就是阻塞式读取元数据,否则 lyrics-fetcher 无法正常工作 -–— 异步读取, lyrics-fetcher 要获取歌词时歌曲的元数据信息还是空,所以会获取不到。
这样的话,在读取歌单时会小卡一下,取决歌单曲目数量。
lyrics-fetcher 配置
最后就是 lyrics-fetcher 的配置了。
(require 'lyrics-fetcher)
;; 歌词存放目录
;; (setq lyrics-fetcher-lyrics-folder "~/Music/lyrics")
;; 歌词获取后端:网易云
(lyrics-fetcher-use-backend 'neteasecloud)
;; 播放歌曲前获取歌词
(add-hook 'emms-player-started-hook
#'(lambda ()
(lyrics-fetcher-show-lyrics nil
:suppress-open t
:suppress-switch nil)))
这样一来,每次播放歌曲的时候就是去拿歌词存到本地,下次就会直接调用。
lyrics-fetcher 还可以下封面,更多操作可以去看文档。我是走的极简,所以不用封面。