[Hacking] 让 EMMS 读取并显示音乐文件内嵌歌词

引言

本人目前将 EMMS 作为主力本地音乐播放器使用,因为 EMMS 不仅可以方便地用于播放列表管理、歌曲评分管理,还能在敲代码时用余光瞄一眼歌词。 EMMS 虽然可以实现外部 .lrc 文件的加载以及网络歌词的下载,但对于自己下载的本地歌词而言,本人还是习惯于将歌词文件嵌入到音乐的标签数据中, 这样不仅可以没有顾虑地随意移动或者重命名音乐文件,还可以让存放音乐的文件夹更加简洁。目前很多播放器都支持内嵌歌词的功能, 但是 EMMS 原生是不支持这个功能的,于是就打算自己添加了。

实现

研究了一段时间,发现只需要修改 2 个函数即可实现: 现在的 EMMS 大部分音乐的元数据主要由 emms-info-native 提供,里面定义了这个列表常量:

(defconst emms-info-native--accepted-vorbis-fields
  '("album"
    "albumartist"
    "albumartistsort"
    "albumsort"
    "artist"
    "artistsort"
    "composer"
    "composersort"
    "date"
    "discnumber"
    "genre"
    "label"
    "originaldate"
    "originalyear"
    "performer"
    "title"
    "titlesort"
    "tracknumber"
    "year")
  "EMMS info fields that are extracted from Vorbis comments.")

这个列表常量用于指定 EMMS 读取的音乐文件的标签类型,我个人不太清楚 EMMS 作者将它定义成常量的意图, 但是不影响修改它,为它增加 "lyrics" 元素:

(push "lyrics" emms-info-native--accepted-vorbis-fields)

至此, EMMS 已经可以读取文件的 info-lyrics 了,但是 emms-info-native 默认标签数据不含有换行的, 而歌词文件中是以换行分割不同时间的歌词的,如:

[00:29.25]过完整个夏天
[00:34.74]忧伤并没有好一些
[00:41.18]开车行驶在公路无际无边
[00:47.32]有离开自己的感觉
[00:52.45]
[00:53.34]唱不完一首歌
[00:59.37]疲倦还剩下黑眼圈
...

因此还需要修改 emms-info-native 中,标签解析函数的正则表达式:

(defun emms-info-native--split-vorbis-comment-multiline (comment)
  (let ((comment-string (decode-coding-string (mapconcat
                                               #'byte-to-string
                                               comment
                                               nil)
                                              'utf-8)))
    (when (string-match "^\\(.+?\\)=\\([\0-\377[:nonascii:]]*\\)" comment-string)
      (cons (downcase (match-string 1 comment-string))
            (match-string 2 comment-string)))))

(advice-add #'emms-info-native--split-vorbis-comment :override #'emms-info-native--split-vorbis-comment-multiline)

现在内嵌歌词就可以被正确地载入至轨道信息中了,下面就可以扩展 emms-lyrics 中查找歌词文件的函数了:

(defconst emms-info-lyrics-temp-file (make-temp-file "emms-info-lyrics-" nil ".lrc"))

(defun emms-lyrics-find-with-info-lyric (file)
  (if-let ((file (emms-lyrics-find-lyric file)))
      file
    (when-let ((embedded-lyric (emms-track-get (emms-playlist-current-selected-track) 'info-lyrics)))
      (with-temp-buffer
        (insert embedded-lyric)
        (write-region (point-min) (point-max) emms-info-lyrics-temp-file nil))
      emms-info-lyrics-temp-file)))

(setq emms-lyrics-find-lyric-function #'emms-lyrics-find-with-info-lyric)
(add-hook 'kill-emacs-hook (lambda () (when (file-exists-p emms-info-lyrics-temp-file) (delete-file emms-info-lyrics-temp-file))))

这里在临时目录创建了一个临时的歌词文件,写入当前播放曲目内嵌的歌词并作为 emms-lyrics-find-with-info-lyric 的返回值, 可以通过 emms-lyrics-visit-lyric 进行访问。这样, EMMS 就可以像其他播放器那样加载内嵌歌词了,并且功能与加载一般歌词文件没有太大区别。

5 个赞

请问您是用什么软件批量获取lrc格式的歌词并打上标签的?我目前用picard,但是其下载的歌词没有时间戳,于是只好使用网易云的api给 lyrics-fetcher 提了个PR来批量下载lrc格式的歌词。

我目前用的是叫做MusicTag的Windows软件,可以一键获取元数据和歌词(可以嵌入也可以是lrc文件),Picard对于中文的曲目还是太麻烦了,很多需要手动选择专辑

忘说了我用Arch,MusicTag貌似是付费软件?Linux上好像没有类似的软件 /叹气

不是开源/自由软件,但是是免费软件,我自己是在Windows虚拟机上运行的:音乐标签pc版 - vinlxc - 博客园

1 个赞

你的完整配置有公开吗? :smiley:

  • ~/.emacs.d/custom-lisp/emms-ext.el
;;; -*- lexical-binding: t -*-

(require 'cl-lib)

(require 'emms)
(require 'emms-info-native)

(add-to-list 'emms-info-native--accepted-vorbis-fields "lyrics")

(defconst emms-info-lyrics-temp-file (make-temp-file "emms-info-lyrics-" nil ".lrc"))

(defun emms-info-native--split-vorbis-comment-multiline (comment)
  (let ((comment-string (decode-coding-string (mapconcat
                                               #'byte-to-string
                                               comment
                                               nil)
                                              'utf-8)))
    (when (string-match "^\\(.+?\\)=\\([\0-\377[:nonascii:]]*\\)" comment-string)
      (cons (downcase (match-string 1 comment-string))
            (match-string 2 comment-string)))))

(advice-add #'emms-info-native--split-vorbis-comment :override #'emms-info-native--split-vorbis-comment-multiline)

(defun emms-lyrics-find-with-info-lyric (file)
  (if-let ((file (emms-lyrics-find-lyric file)))
      file
    (when-let ((embedded-lyric (emms-track-get (emms-playlist-current-selected-track) 'info-lyrics)))
      (with-temp-buffer
        (insert embedded-lyric)
        (write-region (point-min) (point-max) emms-info-lyrics-temp-file nil))
      emms-info-lyrics-temp-file)))

(setq emms-lyrics-find-lyric-function #'emms-lyrics-find-with-info-lyric)

(defun emms-lyrics-delete-temp-file ()
  (when (file-exists-p emms-info-lyrics-temp-file)
    (delete-file emms-info-lyrics-temp-file)))

(add-hook 'kill-emacs-hook #'emms-lyrics-delete-temp-file)
(advice-add #'emms-lyrics-visit-lyric :after #'auto-revert-mode)

(provide 'emms-ext)
;;; emms-ext.el ends here
  • ~/.emacs.d/init.el
(use-package emms
  :ensure t
  :defer t
  :hook (emms-player-started . emms-show)
  :commands emms
  :custom
  (emms-playlist-buffer-name "*Music*")
  (emms-info-asynchronously t)
  (emms-lyrics-scroll-p nil)
  :config
  (emms-all)
  (unless emms-player-list
    (emms-default-players))
  (emms-mode-line-disable))

(use-package emms-ext
  :load-path "custom-lisp"
  :demand t
  :after emms)
2 个赞

我试用了一下,但报如下错误:

Lisp error: (void-variable emms-info-native--accepted-vorbis-fields)

是不是 emms 内部实现改变了?

是的,最新的方案如下:

(defun emms-lyrics-switch-display-position (arg)
  (interactive "P")
  (pcase-let ((`(,on-minibuffer ,on-modeline ,buffer) (ring-next emms-lyrics-display-positions (list emms-lyrics-display-on-minibuffer
                                                                                                     emms-lyrics-display-on-modeline
                                                                                                     emms-lyrics-display-buffer))))
    (unless (eq emms-lyrics-display-on-minibuffer on-minibuffer)
      (emms-lyrics-toggle-display-on-minibuffer))
    (unless (eq emms-lyrics-display-on-modeline on-modeline)
      (emms-lyrics-toggle-display-on-modeline))
    (unless (eq emms-lyrics-display-buffer buffer)
      (emms-lyrics-toggle-display-buffer)
      (when (and arg (buffer-live-p emms-lyrics-buffer))
        (emms-lyrics-buffer-to-new-frame)))))

(cl-pushnew "lyrics" emms-info-native-vorbis--accepted-fields :test #'string=)

(defconst emms-info-lyrics-temp-file (make-temp-file "emms-info-lyrics-" nil ".lrc"))

(defun emms-info-native-vorbis--split-comment-multiline (comment)
  (let ((comment-string (decode-coding-string (mapconcat
                                               #'byte-to-string
                                               comment
                                               nil)
                                              'utf-8)))
    (when (string-match "^\\(.+?\\)=\\([\0-\377[:nonascii:]]*\\)" comment-string)
      (cons (downcase (match-string 1 comment-string))
            (match-string 2 comment-string)))))

(advice-add #'emms-info-native-vorbis--split-comment :override #'emms-info-native-vorbis--split-comment-multiline)

(defun emms-lyrics-find-with-info-lyric (file)
  (if-let ((file (emms-lyrics-find-lyric file)))
      file
    (when-let ((embedded-lyric (emms-track-get (emms-playlist-current-selected-track) 'info-lyrics)))
      (with-temp-buffer
        (insert embedded-lyric)
        (write-region (point-min) (point-max) emms-info-lyrics-temp-file nil))
      emms-info-lyrics-temp-file)))

(setq emms-lyrics-find-lyric-function #'emms-lyrics-find-with-info-lyric)

(defun emms-lyrics-delete-temp-file ()
  (when (file-exists-p emms-info-lyrics-temp-file)
    (delete-file emms-info-lyrics-temp-file)))

(add-hook 'kill-emacs-hook #'emms-lyrics-delete-temp-file)

1 个赞