CD 从抓轨到搭建流媒体服务器 —— 以《月临寐乡》为例

2022-07-19

v0.0.1

由于某些原因,进了 Static World 的群并入坑了 月临寐乡 ,梦开始了。作为幻想乡的新人,也算是有了自己喜欢的社团。但是更细节的东西,狐狐脑子一下子塞不下那么多东西,只能慢慢探索了惹。

所以,关于音频格式、元数据、 alsa 、 ffmpeg(待续) 的部分应该有好多错误(拍飞),大佬们多批评。

缘起

之前在 幻想遊園郷Memories of a Town 的时候并没有注意过 tag 的问题,而是用 K3B 抓轨后将纯粹的 wav 算了 md5sum 就扔进曲库了。但是这次显然要做精致一点。

月2

wav 格式的音频文件支持元数据吗?答案是不支持。但是这并不意味着不可以嵌入 ID3 tag 。既然能嵌入 ID3 tag 那么就一定可以嵌入专辑封面,但是无语的地方在于 Windows 并不能识别 wav 里的 ID3 tag,如果你加入了 ID3 tag ,虽然 VLC 、 mpv 、 mplayer 都可以认到,但是 Windows 资源管理器、 Groove 、 Windows Media Player 甚至 Audacious 都是认不到的,它们只会去读 wav 的一个 RIFF INFO,而且 Windows 资源管理器、 Groove 、 Windows Media Player 会在 UTF-16/UTF-8 乱码。

当然 RIFF INFO 就没有专辑封面了,所以在 Windows 默认的几个播放器以及资源管理器都将无法显示专辑封面。如果你并不 care Windows 的操作,那么你可以在 Linux 下欢快地享受 ID3 tag 的强大。

当然另一个解决方法是将其转换为 flac 等其他无损格式,当然这也就是另一个话题了。

抓轨

有一台光驱就能抓。

我在 Debian 下使用了 K3B ,默认设置即可,将得到 wav 格式的文件。

如果在 Windows 下也可以使用群友推荐的 EAC 即 Exact Audio Copy ,支持从 freedb 拉取元数据,也支持 AccurateRip ,就不用自己敲元数据了。当然对于比较早拿到新碟的来说,远程数据库大概率也是没有。

导入元数据

Kid3 和 Mp3tag 是我尝试下来最好用的两个软件,分别也是 Linux 和 Windows 下比较好的解决方案。这里列举了几个常见可行的方法,大家也可以一一尝试。

Kid3

Kid3 作为 K 家的软件,其功能强大自不必说。支持 Linux 和 Windows ,可以批量编辑,操作非常灵活,我主要就用它。 ID3 相关功能方面,支持 ID3 tag 的编辑并且可以在 ID3v1.1 、 ID3v2.3 和 ID3v2.4 之间一键转换。

在 File->Open Folder 就可以导入整个文件夹的曲目,并且全选曲目再在编辑框中编辑就可以批量编辑,也可以批量转换 tag 版本。

在编辑区, Tag2 部分就是 ID3v2.x 的编辑区域,在这里也可以插入专辑封面,而且看起来并没有图片大小限制;Tag3 部分是 RIFF INFO 的编辑区域,是的,它支持 RIFF INFO ,可惜是 UTF-16 编码,在 Windows 直接乱码,只能回到 Windows 来解决。、

月2 with Kid3

月2 批量编辑

Mp3tag

Windows only (不要跟我说可以 wine)。

Mp3tag 是一个在 Windows 下常用的 tag 编辑器,默认会添加 ID3 tag 和 RIFF INFO 且没有编码问题,可以被 Windows 资源管理器和 Groove 、 Windows Media Player 正常识别。但是添加的专辑封面由于在 ID3 tag 中,依然无法被识别。

还记得 Kid3 编辑的 RIFF INFO 会有编码问题吗,一个简便但是奇怪的方法就是在 Kid3 中编辑好元数据和封面,然后再到 Mp3tag 打开,重新保存,这样 Mp3tag 会将元数据重新写入成可以被 Windows 识别的编码。

Mp3tag 默认支持从 MusicBrainz 和 freedb 检索元数据,另外 THBWiki 提供了一个 API 来检索东方相关专辑和曲目和获取资料,并提供了一个 Mp3tag 插件来自动填入 ID3 tag ,其帮助页面介绍了如何使用该插件。

Mp3tag with THBWiki

以 Windows10 为例,首先下载 THBWiki.src ,将其放到 %appdata%\Mp3tag\data\sources 目录下。启动 Mp3tag ,在“Tag Sources”下拉框下就可以找到 THBWiki 的选项。

月1 with Mp3tag

注意此时图中显示了歌曲元数据是因为我之前有添加过。

月2 with THBWiki

在搜索框中搜索后,将会返回搜索结果,检查后点击 OK 即可。

月2 new tags with THBWik

所有信息将被加入并保存,仔细看应该可以看出元数据已经变掉了。

Audacity

Audacity 是 Linux 下一个著名的音频编辑软件,将曲目导入,在导出的时候就可以编辑元数据。或者在 Edit->Metadata 编辑,但是不能插入专辑封面。同样是支持 Linux 和 Windows ,但是不是很推荐这个软件啦。首先它本身不是一个专门编辑元数据的软件,其次它的元数据编辑功能完全可以被 Kid3 代替,甚至生成的 RIFF INFO 在 Windows 的表现还不如 Kid3 。

EasyTAG

EasyTAG 支持的格式也非常广泛,就是不支持 wav (*_*)。

puddletag

puddletag 也是不支持 wav ,别的格式可以考虑用一下啦。

foobar2000

foobar2000 是 windows only,很多人用,但是我试了试感觉巨难用。

流媒体服务器

这里选择的是 icecast2 ,这是一个比较流行的流媒体服务器软件。最新的 Release 是 Release 2.5.0-beta3 ,但是遗憾的是它依然是 beta ;最近的稳定版是 2018 年释出的 2.4.4 。

icecast 2.4.4

我的服务链接,基于 2.4.4 版本。

如果你在比较新的 Debian 或者 Ubuntu 上安装,都将会安装上 2.4.4 的版本:

$ sudo apt-get install icecast2

打包者为我们做好了大部分配置工作。在 Debian11 上,会自动添加 icecast 用户组和 icecast 用户,这是由于 icecast2 是默认由 icecast 用户启动的。自启动 demon 放在是 /etc/init.d/icecast2 ,配置文件是 /etc/icecast2/icecast.xml ,可以发现 /etc/icecast2/icecast.xml 的所有者也是 icecast 。

icecast 2.4.4 的配置比较简单,可以参考官网的2.4.1文档以及 FAQ

对于简单的配置:

  • <location><admin> 只用于 Web 端显示,设置即可
  • <source-password> 用于推流时使用; <relay-password> 用于中继,但是由于只有一台服务器所以用不到;<admin-user><admin-password> 用于 Web 页面的管理员登录
  • 默认监听 0.0.0.0:8000 ,如果需要更改则在配置中的 <listen-socket> 指定 <port><bind-address><listen-socket> 可以有多个
  • 使用 systemctl 重启 systemctl restart icecast2.service ,或在非 systemd 的系统上 sudo service icecast2 restart

如果出现了 UTF-8 乱码,可以参考 <mount> 的配置:

<mount type="normal">
    <mount-name>/sw1</mount-name>
    <charset>UTF8</charset>
</mount>

另外如果希望你的流媒体服务可以在 icacast2 的列表中被搜索到,可以选择加入如下的配置:

<!-- Uncomment this if you want directory listings -->
<directory>
    <yp-url-timeout>15</yp-url-timeout>
    <yp-url>//dir.xiph.org/cgi-bin/yp-cgi</yp-url>
</directory>

通常在一台服务器上,我们会同时开多个服务,但是 80 端口只有一个,这时候就可以使用 nginx 来作为 proxy 根据规则转发请求,同时配置简单的限流措施。尽管有人给出了一个非常全的 nginx 配置 ,然而给 icecast2.4.x 套一个 nginx 后,尽管网页可以打开,远程推流将无法正常进行(当然直接在服务器上不过 nginx 推是可以的),在很多主流播放器(比如 VLC)上收听也会断断续续。

所以我只能用 stream 来实现:

stream {
        limit_conn_zone $binary_remote_addr zone=sperip:2M;

        upstream icecast {
                server 127.0.0.1:50110;
        }

        server {
                listen 2011;
                listen [::]:2011;

                limit_conn sperip 5;

                proxy_ssl off;

                proxy_pass icecast;
        }
}

糟糕的是,如果这样写, icecast2 生成的 XSFP 和在 dir.xiph.org 上的端口都将显示成 50110 而不是 2011 ,意义不是很大。

所以我只能尝试更新的 2.5.0-beta3 。

icecast 2.5.0-beta3

实践证明在 Debian11 下 2.5.0-beta3 在 nginx 代理的情况下,推流会频繁掉线,所以如果没啥兴趣没必要看这部分了。 Icecast2 官方给出了两个关于 nginx 代理的页面,分别为 known reverse proxy restrictionsknown https restrictions 。 还可以参考这篇笔记,有趣的是这篇笔记在 icecast 2.5.0 下成功用 nginx 代理并给出了配置,不知道我没有成功是否和 nginx 的版本也相关。最后我还是摆了,让 Icecast2 直接监听在了 0.0.0.0:2011

从官方下载源码包,这里放上链接: tarballzip ball,编译三部曲如下:

$ ./configure
$ make
$ sudo make install

但是我并没有这么干,因为第一步就没过(bushi)。

其实 debian 打过这个包,把它的编译脚本拿来抄作业就好了,找到 2.4.4 的 tarball ,这里给出北外源的链接,在 debian/control 就可以看到编译依赖。或者把 debian 目录放到新的 2.5.0-beta3 的源码目录下,直接打 debian 包不香吗。

打包前记得修改包版本,修改 changelog 即可:

$ dch -s

icecast2 (2.5.0-beta3) unstable; urgency=high

  * 信息自己写,这只是个示例

 -- weilinfox <[email protected]>  Sun, 17 Jul 2022 16:21:13 +0000

没想到的是,测试没有过。首先 icecast2 在 2.5.0-beta2 引入了测试,测试放在了 tests/ 目录下,可以切换过去并 make check-TESTS 运行测试;其次, icecast2 不允许使用 root 用户运行,所以偷懒用 root 打包会直接测试失败;再次,测试需要依赖 ffmpeg ,如果你的环境没有,需要单独安装;最后,即使你都注意到了,其中有 4 个测试是无法通过的,虽然看起来对功能影响不大。

测试记录如下:

FAIL: admin.test 5 - buildm3u-user
FAIL: admin.test 6 - buildm3u-fakeuser
FAIL: admin.test 46 - mount-sourceauth
FAIL: admin.test 57 - on-connect-test-sourceauth


============================================================================
Testsuite summary for Icecast 2.4.99.3
============================================================================
# TOTAL: 61
# PASS:  57
# SKIP:  0
# XFAIL: 0
# FAIL:  4
# XPASS: 0
# ERROR: 0
============================================================================
See tests/test-suite.log
Please report to [email protected]
============================================================================

确认了问题不大后,我将 admin.test 的测试取消了。在 tests/Makefile.am 和 中可以看到下面的三行:

TESTS = \
    startup.test \
    admin.test

直接改成:

TESTS = \
    startup.test

dpkg-buildpackage 打包就可以了。

打包命令简单写在下面:

$ mk-build-deps
$ sudo apt-get install ./icecast2-build-deps_2.5.0-beta3_all.deb
$ dpkg-buildpackage -b -uc -us

在上级目录可以找到 debian 包 icecast2_2.5.0-beta3_amd64.deb

注意 2.5.0-beta3 并不能直接使用 2.4.4 的配置文件,配置理论上应该参考官网的 2.5.0 文档,但是它指向的似乎……还是 2.4.1 的文档啊,甚至 tarball 中的 doc 也是老的文档。事实上有些选项已经不适用了,所以只能把 conf/icecast.xml.in 或者打完 debian 包后生成的 /etc/icecast2/icecast.xml 文件做为模板,重新写配置,大部分配置还是一样的,已经改变的地方只能从注释中找寻蛛丝马迹。

推流

DarkIce

DarkIce 是一个音频推流工具,它从声卡或其他音频设备采集声音,然后编码并推送,支持 IceCast 1.3.x 和 2.x 。最新 Release 1.4 。

由于 Debian 和 Ubuntu 源中均为 1.3 版本,不兼容 2.5.0-beta3 的协议,如果使用了 2.5.0-beta3 的服务器就需要自行编译 1.4 版本。

$ sudo apt-get install darkice

默认配置文件在 /etc/darkice.cfg ,这个文件通常需要自己创建,可以查看帮助文档:

$ man darkice
$ man darkice.cfg

这里给出一个配置文件的示例:

[general]
duration        = 0
bufferSecs      = 10
reconnect       = yes

[input]
device          = default
sampleRate      = 44100
bitsPerSample   = 16
channel         = 2

[icecast2-0]
format          = mp3
bitrateMode     = vbr
#bitrate         = 1411
quality         = 0.8
server          = sw.inuyasha.love
port            = <port>
password        = <your password>
mountPoint      = <mount point>
sampleRate      = 44100
channel         = 2
name            = 白玉製作所 channel 1
description     = 白玉製作所 Audio Streaming Channel 1
url             = //sw.inuyasha.love:2011/<mount point>
genre           =
public          = yes
localDumpFile   = /tmp/live_sw.mp3
fileAddDate     = no
#fileDateFormat  =
#lowpass         =
#highpass        =

bitrateMode = vbr 的好处在于,可以根据数据本身的情况动态调整码率,在保证质量的前提下节约了带宽。

最主要的坑就在于 [input] 下的 device ,这个设备可以是 OSS DSP , ALSA 设备, PalseAudio 设备,或者 Jack 设备。这里的 default 是默认的 ALSA 设备,如果测试不能使用就需要根据具体情况修改,后面将会提到。

编译 DarkIce 1.4 的过程和前面编译 Icecast2 类似,首先下载 DarkIce Relase 1.4,然后下载 debian 打包 1.3 时使用的脚本,这里同样给出北外源的链接。同样,将 debian 目录移动到 darkice 源码目录。

修改包版本,修改 changelog 即可:

$ dch -s

darkice (1.4) experimental; urgency=high

  * Compiled with C++11 stadard
  * 信息自己写,这只是个示例

 -- weilinfox <[email protected]>  Mon, 18 Jul 2022 21:32:10 +0800
$ mk-build-deps
$ sudo apt-get install ./darkice-build-deps_1.4_amd64.deb
$ dpkg-buildpackage -b -uc -us

DarkIce1.4 在构建时可能出现一个常见的编译错误,我在 g++11.2 复现如下:

In file included from Connector.h:39,
                 from Connector.cpp:33:
Referable.h:102:57: error: ISO C++17 does not allow dynamic exception specifications
  102 |         ~Referable ( void )                             throw ( Exception )
      |                                                         ^~~~~
Referable.h:121:57: error: ISO C++17 does not allow dynamic exception specifications
  121 |         increaseReferenceCount ( void )                 throw ( Exception )
      |                                                         ^~~~~
Referable.h:139:57: error: ISO C++17 does not allow dynamic exception specifications
  139 |         decreaseReferenceCount ( void )                 throw ( Exception )
      |                                                         ^~~~~
Referable.h: In destructor ‘virtual Referable::~Referable()’:
Referable.h:105:17: warning: ‘throw’ will always call ‘terminate’ [-Wterminate]
  105 |                 throw Exception( __FILE__, __LINE__,
      |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  106 |                                  "reference count positive in destructor",
      |                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  107 |                                  referenceCount);
      |                                  ~~~~~~~~~~~~~~~
Referable.h:105:17: note: in C++11 destructors default to ‘noexcept’
In file included from Connector.h:40,
                 from Connector.cpp:33:
Ref.h: At global scope:
Ref.h:114:49: error: ISO C++17 does not allow dynamic exception specifications
  114 |         Ref ( const Ref<T> &    other )         throw ( Exception )
      |                                                 ^~~~~
Ref.h:127:49: error: ISO C++17 does not allow dynamic exception specifications
  127 |         Ref ( T   * obj )                       throw ( Exception )
      |                                                 ^~~~~
Ref.h:139:49: error: ISO C++17 does not allow dynamic exception specifications
  139 |         ~Ref ( void )                           throw ( Exception )
      |                                                 ^~~~~
Ref.h:150:49: error: ISO C++17 does not allow dynamic exception specifications
  150 |         operator->() const                      throw ( Exception )
      |                                                 ^~~~~
Ref.h:167:49: error: ISO C++17 does not allow dynamic exception specifications
  167 |         operator= ( Ref<T>  other )             throw ( Exception )
      |                                                 ^~~~~
Ref.h:181:49: error: ISO C++17 does not allow dynamic exception specifications
  181 |         operator= ( T*  obj )                   throw ( Exception )
      |                                                 ^~~~~
Ref.h:195:49: error: ISO C++17 does not allow dynamic exception specifications
  195 |         set ( T   * newobj )                    throw ( Exception )
      |

如果出现了相同的错误,可以将版本切为 C++11 ,可以在 debian/rules 的开头添加一行:

DEB_CXXFLAGS_MAINT_APPEND := -std=c++11

重新构建即可。

构建成功后可以对 1.3 平滑升级,不需要更改任何配置。

ALSA (The Advanced Linux Sound Architecture)

要不是这个专我大概这辈子都不会去碰这个东西

为啥会扯到 ALSA 呢,还记得 DarkIce 要采集吗,推流的时候需要把播放器播放的重新采集编码,所以这里使用 Loopback 虚拟声卡设备。

曾经试过 Loopback + palseaudio ,但是 palseaudio 不太稳定。关于 palseaudio 可以使用 pacmd list-sink-inputspacmd list-source-outputs 查看输入和输出的源,实测 mplayer 会随机从 snd_aloop 设备掉到默认声卡设备。最终直接采用 ALSA Loopback sound card 。

需要安装 alsa 工具:

$ sudo apt-get install alsa-utils

使用 aplay -L 查看现有设备:

$ aplay -L

null
    Discard all samples (playback) or generate zero samples (capture)
default
    Playback/recording through the PulseAudio sound server
lavrate
    Rate Converter Plugin Using Libav/FFmpeg Library
samplerate
    Rate Converter Plugin Using Samplerate Library
speexrate
    Rate Converter Plugin Using Speex Resampler
jack
    JACK Audio Connection Kit
oss
    Open Sound System
pulse
    PulseAudio Sound Server
upmix
    Plugin for channel upmix (4,6,8)
vdownmix
    Plugin for channel downmix (stereo) with a simple spacialization
hw:CARD=RK809,DEV=0
    Analog RK809, fe410000.i2s-rk817-hifi rk817-hifi-0
    Direct hardware device without any conversions
plughw:CARD=RK809,DEV=0
    Analog RK809, fe410000.i2s-rk817-hifi rk817-hifi-0
    Hardware device with all software conversions
sysdefault:CARD=RK809
    Analog RK809, fe410000.i2s-rk817-hifi rk817-hifi-0
    Default Audio Device
dmix:CARD=RK809,DEV=0
    Analog RK809, fe410000.i2s-rk817-hifi rk817-hifi-0
    Direct sample mixing device
usbstream:CARD=RK809
    Analog RK809
    USB Stream Output

如果只显示为 null ,也就是没有声卡设备,那么需要排查自己的用户是否在 audio 用户组。如果 sudo aplay -L 可以看到声卡设备,则你的用户大概率不在 audio 用户组,把自己的用户加入该组后重新登录:

$ usermod -a -G audio <your_username>
$ exit

如果你的用户确实在 audio 用户组,且 sudo aplay -L也没有声卡设备,那你可以创建虚拟声卡,需要载入相关内核模块:

$ sudo modprobe snd-dummy
$ sudo aplay -L

null
    Discard all samples (playback) or generate zero samples (capture)
hw:CARD=Dummy,DEV=0
    Dummy, Dummy PCM
    Direct hardware device without any conversions
plughw:CARD=Dummy,DEV=0
    Dummy, Dummy PCM
    Hardware device with all software conversions
default:CARD=Dummy
    Dummy, Dummy PCM
    Default Audio Device
sysdefault:CARD=Dummy
    Dummy, Dummy PCM
    Default Audio Device
dmix:CARD=Dummy,DEV=0
    Dummy, Dummy PCM
    Direct sample mixing device

如上显示则虚拟声卡设备正常,可以将这一行加入 /etc/modules ,使系统启动时自动载入该内核模块:

$ echo snd-dummy | sudo tee -a /etc/modules

检查声卡设备正常后,就可以使用 Loopback 设备,载入相关的内核模块:

$ sudo modprobe snd-aloop
$ aplay -L

# 应当多出来下面的设备
hw:CARD=Loopback,DEV=0
    Loopback, Loopback PCM
    Direct hardware device without any conversions
hw:CARD=Loopback,DEV=1
    Loopback, Loopback PCM
    Direct hardware device without any conversions
plughw:CARD=Loopback,DEV=0
    Loopback, Loopback PCM
    Hardware device with all software conversions
plughw:CARD=Loopback,DEV=1
    Loopback, Loopback PCM
    Hardware device with all software conversions
sysdefault:CARD=Loopback
    Loopback, Loopback PCM
    Default Audio Device
front:CARD=Loopback,DEV=0
    Loopback, Loopback PCM
    Front output / input
surround21:CARD=Loopback,DEV=0
    Loopback, Loopback PCM
    2.1 Surround output to Front and Subwoofer speakers
surround40:CARD=Loopback,DEV=0
    Loopback, Loopback PCM
    4.0 Surround output to Front and Rear speakers
surround41:CARD=Loopback,DEV=0
    Loopback, Loopback PCM
    4.1 Surround output to Front, Rear and Subwoofer speakers
# 这里省略后面的输出

如果可以看到 Loopback 设备则成功,可以将该内核模块加入 /etc/modules ,使系统启动时自动载入该内核模块:

$ echo snd-aloop | sudo tee -a /etc/modules

观察 Loopback 声卡的设备信息可以看到, hw:CARD=Loopbackplughw:CARD=Loopback 都有 DEV=0DEV=1 两个设备,实测它们的行为就像管道一样,一端输入一端采集即可。

hw:CARD=Loopback 为例, DarkIce 的配置改为 device = hw:CARD=Loopback,DEV=1 , aplay 的播放命令为 aplay -D hw:CARD=Loopback,DEV=0 xxxx.wav

这里给出我的定时推流脚本以及 DarkIce 的 [input] 部分配置:

#!/bin/bash

# 开始时间 18:30
START_TIME="1830"
# 结束时间 21:00
# 每一轮播完才会检查结束时间
END_TIME="2100"

start=0
killall darkice

while true; do
        time_now=$(date +%H%M)
        if [ "${time_now}" -lt "${START_TIME}" ] || [ "${time_now}" -ge "${END_TIME}" ]; then
                [ "${start}" != "0" ] && killall darkice && echo 'Stop broadcast now.'
                start=0; sleep 5s; continue
        fi
        if [ "${start}" == "0" ] && [ "${time_now}" -ge "${START_TIME}" ]; then
                start=1
                darkice -c /etc/darkice.cfg &
                echo Start broadcast now

                # 开播前放两次 攻撃戦
                for i in $(seq 2); do
                        aplay -D hw:CARD=Loopback,DEV=0 /home/hachi/Music/North\ Korean\ Archives\ -\ 攻撃戦だ.wav
                done

                sleep 5s
        fi

        aplay -D hw:CARD=Loopback,DEV=0 /home/hachi/Music/SW/月溯莲台/*.wav
        sleep 5s
        aplay -D hw:CARD=Loopback,DEV=0 /home/hachi/Music/SW/月临寐乡/*.wav
        sleep 5s
done

或者简单粗暴一点:

#!/bin/bash

while 1; do
        aplay -D hw:CARD=Loopback,DEV=0 /home/hachi/Music/SW/月溯莲台/*.wav
        aplay -D hw:CARD=Loopback,DEV=0 /home/hachi/Music/SW/月临寐乡/*.wav
done
# /etc/darkice.cfg
# 只给出 input 部分作为播放脚本的参考
# hw:CARD=Loopback,1 和 hw:CARD=Loopback,DEV=1 含义一致
[input]
device          = default
device          = hw:CARD=Loopback,1
sampleRate      = 44100
bitsPerSample   = 16
channel         = 2

by SDUST weilinfox