[Emacs] EMMS: 媒体播放器

简介

EMMS 全称 Emacs Multimedia System ,即 Emacs 的多媒体系统,用于在 Emacs 中展示和播放多媒体 [1] ,可以接入多种后端。我把它用来做音乐播放器,离入魔又近了一步。

EMMS 有非常详细的文档,用于参阅检索:《The Emms Manual》。

安装

可以直接用 package.elM-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-minimalisticemms-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
  • <, 倒退 10s
  • d, 将光标所在行的曲目移出播放列表
  • 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-modelinetnil.

我看配置可以配置一个单独的 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 还可以下封面,更多操作可以去看文档。我是走的极简,所以不用封面。

Powered by Org Mode.