4

【.NET 与树莓派】用 MPD 制作数字音乐播放器

 2 years ago
source link: https://www.cnblogs.com/tcjiaan/p/15528276.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【.NET 与树莓派】用 MPD 制作数字音乐播放器

树莓派的日常家居玩法多多,制作一台属于自己的数字音乐播放机是其中的一种。严格上说,树莓派是没有声卡的,其板载的 3.5 mm 音频孔实际是通过 PWM 来实现音频输出的(通过算法让PWM信号变成模拟信号)。在 Pi 4 上输出的音质还算过得去,至少没有杂音(如果有杂音,俗称电流声,其实电流是没有声音的,只是供电电压的不稳定产生了模拟信号,并不幸地进入了喇叭使它发出莫名的响声),就是低音不够厚高音有点飘,不追求 HiFi 音质只是看看恐怖片的话是没问题的。

正是因为使用 PWM 产生音频信号,所以,如果 GPIO 上要用 PWM ,就不能使用音频了。当然,通过GPIO引脚也能输出音频,因为 PI 有两路 PWM 输出,正好,一路输出左声道,另一路输出右声道。但一般咱们不会这么玩,主要还是音质问题。你也可以直接买个 USB 声卡,也很方便,音质好不好取决于你剁手的能力。不过呢,若真想享受一下自己 DIY 播放机,最好还是买一块 I2S 解码板,树莓派是支持 I2S 协议的。在 /boot/config.txt 文件中,要加上这一句来开启:

dtparam=i2s=on

然后,还要配置 I2S 扩展板的类型,一般使用 HifiBerry DAC 即可,多数扩展板是兼容的。

dtoverlay=hifiberry-dac

最好把板载的音频也禁用(禁用后不仅 3.5 接口不能用,连 HDMI 也不能输出音频的)。

dtparam=audio=off

最后保存 config.txt。

树莓派的官方系统默认自带 ALSA 相关支持的,但是,如果你写 C++ 代码时要使用 asoundlib 的话,需要安装开发者专用的包。

sudo apt install libasound2-dev libasound2-doc

doc 是帮助文档,一起装上也方便。

执行 aplay -L 命令你就会看到 hifiberry DAC 了。

 因为板载的音频被禁用,并且没有接入 USB 声卡,所以系统默认只能选择 I2S 扩展板。所以,你不需要再做其他配置,如果在执行一些播放命令时要选用声音设备(如 aplay、gmediarender 等),可以直接用 default 或 sysdefault 来引用。

-------------------------------------------------------------------------------------------------------------------

上面说了那么多,那咱们怎么打造私人播放机呢。其实,有现成的系统的,如 Volumio、MoOdeAudio 等。你要是懒得折腾,可以直接用,但是:

1、Volumio 的新版本 bug 相当地多,而且很不稳定。最可恨的是限制越来越多,你还得充值信仰开会员才能用。果断 PASS;

2、MoodeAudio 倒是做得不错,功能完整,无需充信仰。可是对老周来说感觉它功能太多了。

3、作为码农,老是要犯职业病的——为啥不自己开发一个呢,自己用的话,不用做太多的面子工程(比如界面美化),可以专注于功能,满足自己需求即可。

参考下一些数播系统的源代码,其实他们也是借助现有命令工具的,然后做个 Web 应用,在树莓派上以服务器角色运行,然后,你随便什么设备都能够通过浏览器来控制它。所以咱们用 ASP.NET 当然也可以做。你要是想练练手,不妨把 WPF 版本,Xamarin 版本也做做。

也可以考虑把控制逻辑以 Web API 的方式实现,这样客户端你将来想怎么扩展都行。

后台控制最简单的方法就是调用 aplay 命令,使用 Process 类 Start 一个进程,执行 aplay 命令,参数是要播放的音频文件,这实现起来很简单;缺点是管理功能不强,所以,很多开源的数播系统都选用 MPD。也就是 Music Player Daemon,说通俗一点,它就是一个后台运行的服务,客户端向它传递命令,以控制它的播放行为(可以播放指定曲目,可以暂停,可以停止播放器)。可以认为就是个命令方式驱动的音乐播放器,只是以 C / S 架构来运行。

执行下面的命令安装 MPD。

sudo apt install mpd mpc

mpd 是必须的,因为它是服务进程;mpc 是一个简单的客户端程序,以命令行方式使用,用 Socket 通信,可以在本机使用,也可以远程使用。

如果你不要这个简易客户端,那只安装 mpd 即可。

咱们就是自己编程来实现通信的,故而不用 mpc 也行。这个不难的,你会 TCP 编程就行,稍后老周会演示一个例子。

我们现在要做的是配置 mpd ,默认情况下 mpd 是不能正确运行的,咱们必段修改一下配置。配置文件位于 /etc 目录下,名为 mpd.conf。这个文件是面向整个系统配置的,可以跨用户,若要单个用户使用,可以在特定用户的 home 下面建个 .mpd 子目录,再把 mpd.conf 复制进去。但是,咱们是把树莓派做数播机器用的,没必要搞那么多用户配置,直接使用 /etc/mpd.conf 就行。接下来就是修改这个文件。

sudo nano /etc/mpd.conf

选项很多,上面也有注释。咱们只需关注这几个重要的就行。

1、配置音乐文件所存放的目录。

music_directory         "/home/pi/music"

我是把音频文件放在 pi 用户下的music目录中,你可以按实际情况修改。

2、播放列表的存放目录。

playlist_directory              "/home/pi/music/playlists"

为了省事,我直接在 music 目录下建一个新目录,命名为 playlists。

3、数据库文件的存放路径(完整路径,包括目录和文件名)

db_file                 "/home/pi/music/tag_cache"

也是图省事,直接放 music 目录下。文件名是 tag_cache。这个数据库用来保存歌曲的信息的,主要从音频文件的 TAG 标记中获取,就是我们平时查看文件属性时看到那些信息,比如曲目、标题、艺术家、专辑名称等。

 由于编码问题,显示出来的是 ????。

4、日志文件的存放路径。

log_file                        "/home/pi/mpd/mpd.log"

5、存储进程ID的文件(pid 文件)

pid_file                        "/home/pi/mpd/pid"

这个最好改到 pi 目录下,这样不需要 root 权限就能读写,免去后期各种改权限的麻烦。

6、修改状态数据的文件路径。

state_file                      "/home/pi/mpd/state"

7、sticker file ,这个注释上说是存放为歌曲附加的动态信息用的,具体是啥玩意儿老周也不清楚。为了统一管理,为了减少后面出错,还是改一下吧。

sticker_file                   "/home/pi/mpd/sticker.sql"

8、修改运行用户。

user                            "pi"

这个改为 pi,没必要动不动就 root。

9、配置绑定的本机地址。

bind_to_address         "any"

这个选项是必须改的,很重要,用来选定本地绑定的地址,给 TCP 服务器端侦听用的,这个就不必多解释了,折腾过 TCP 通信的话你都懂的。使用 any 表示绑定本机所有地址,如果你只想限制在本机访问,远程不允许连接,可以改为 127.0.0.1;如果你的 Pi 连接网络后有分配固定 IP 的话,可以改为相应的 IP,例如 192.168.0.125。

10、侦听端口。上面配置的是侦听地址,这里是端口,可以不改,默认 6600。

port              "8855"

注释掉的话就是用默认值 6600。

11、自动更新数据库。这个还是设置为 yes 较好。

auto_update    "yes"

这样一来,如果前面配置的音乐目录下的文件有变动,会自动更改数据库。

12、配置音频输出设备。此处配置 audio-output 节点。前面咱们都把板载音载禁用了,所以 pulse audio 就无法用了,只能选 ALSA 方案。

audio_output {
        type            "alsa"
        name            "My ALSA Device"
        device          "default"       # optional
#       mixer_type      "hardware"      # optional
#       mixer_device    "default"       # optional
#       mixer_control   "PCM"           # optional
#       mixer_index     "0"             # optional
}

type 字段要设为 alsa,name 字段你可以随便起个名字;device 字段就是前面用 aplay -L 看到的 hifiberry dac 的设备名,因为现在它已成为系统默认选用的设备,所以用 default 就能引用,或者用 hw:0,1 也行。剩下那几个是可选的,不管它。

配置完后,按【ctrl + o】写入,【Ctrl + x】退出。

我们还要创建一下目录和文件,就是上面在配置文件中出现的几个目录和文件。

先切换到 home 目录。

cd ~

然后创建 music 目录以及子目录

mkdir -p music/playlists

加个 -p 参数是为了能一次性创建多级目录,毕竟咱们创建了 music 目录和 playlists 子目录。

现在,music 目录已经存在了,所以我们直接在它下面创建 tag_cache 文件,就是上面配置的数据库文件。不需要有数据,只要有这个文件就行了,这时候可以使用 touch 命令,具体用法网上随便一搜就有。这个命令本来是用来更新目录或文件的时间的,但它有个特点——如果文件不存在,会自动新建。

touch music/tag_cache

在 pi 的 home 下再建一个 mpd 目录(也是上面配置文件中提及的)。

mkdir mpd

同样的方法,用 touch 命令创建这些文件。

touch mpd/mpd.log
touch mpd/pid
touch mpd/state
touch mpd/sticker.sql

其实这里面只要创建 pid 和 state 这两个文件就行了,其他的 MPD 会自己创建,除非你运行服务时发现报错。

这一通配置之后,mpd 就能用了,重启一下服务,让它加载新的配置。

sudo systemctl restart mpd

执行一下这条命令,看看状态。

sudo systemctl status mpd

能看到绿油油的 running,那就好了。

要测试,你得有音频文件,mp3、wav、flac、ape 等格式的都行,mp3 和 aac 是有损文件,要 HiFi 的话最好 Pass 掉,WAV 和 Flac 都不错。I2S 扩展板一般都支持硬解码,包括 DTS ,也能直接播放。

准备的测试文件不要太少,起码有七、八个,这样才能感觉到效果。准备好文件后,通过 scp 命令上传到树莓派上,放到你前面配置的音乐文件目录中。

scp *.wav [email protected]:/home/pi/music

第一个参数是把当前目录下所有 WAV 文件上传;第二个参数是树莓派上的存放路径,这里可以敲上绝对路径,也可以用 ~ 来代替 home 目录,即 [email protected]:~/music。

回到树莓派的终端,cd 到 music 目录下,ls 一下,就会看到音频文件了。

367389-20211109161907222-711002684.png

------------------------------------------------------------------------------------------------------

好了,基本的测试条件已满足,下面老周再告诉你怎么通过编程来控制 MPD。

控制方式:通过 TCP 协议直接发送命令文本,每条命令末尾要有换行符(\n)。比如,要让 MPD 播放音乐,先 connect 服务器,然后发送“play\n”。通信方式有点像串口交互。

这样的控制方式是不是很好弄?那具体我们能用啥命令呢?如果你在 apt install 时有安装 mpc 的话,你可以在终端中输入:

mpc help

然后你就会看到所有命令了。比如,要列出音乐目录下的所有文件,可以这样执行:

mpc -h 192.168.0.106 listall

-h 参数指定的是服务器(MPD运行的主机)地址(主机名或IP地址均可),listall 就是命令了。

再例如,要停止播放音乐,执行:

mpc -h localhost stop

如果我们要自己编程呢,那就

1、Connect HOST;

2、发送文本 stop \n;

3、要是没别的事,最好关闭连接,等需要时再连接。

总结起来就是:我们只要以文本格式发送命令部分即可。例如,mpc -h localhost play,那么我们的代码只发送 play 就行了,不要带 mpc -h XXXX。注意最后有换行符。

查看有效命令的另一个方法是查看 MPD 的源代码(C++),在 /src/command/AllCommands.cxx 文件中。

static constexpr struct command commands[] = {
    { "add", PERMISSION_ADD, 1, 2, handle_add },
    { "addid", PERMISSION_ADD, 1, 2, handle_addid },
    { "addtagid", PERMISSION_ADD, 3, 3, handle_addtagid },
    { "albumart", PERMISSION_READ, 2, 2, handle_album_art },
    { "binarylimit", PERMISSION_NONE, 1, 1, handle_binary_limit },
    { "channels", PERMISSION_READ, 0, 0, handle_channels },
    { "clear", PERMISSION_PLAYER, 0, 0, handle_clear },
    { "clearerror", PERMISSION_PLAYER, 0, 0, handle_clearerror },
    { "cleartagid", PERMISSION_ADD, 1, 2, handle_cleartagid },
    { "close", PERMISSION_NONE, -1, -1, handle_close },
    { "commands", PERMISSION_NONE, 0, 0, handle_commands },
    { "config", PERMISSION_ADMIN, 0, 0, handle_config },
    { "consume", PERMISSION_PLAYER, 1, 1, handle_consume },
#ifdef ENABLE_DATABASE
    { "count", PERMISSION_READ, 1, -1, handle_count },
#endif
    { "crossfade", PERMISSION_PLAYER, 1, 1, handle_crossfade },
    { "currentsong", PERMISSION_READ, 0, 0, handle_currentsong },
    { "decoders", PERMISSION_READ, 0, 0, handle_decoders },
    { "delete", PERMISSION_PLAYER, 1, 1, handle_delete },
    { "deleteid", PERMISSION_PLAYER, 1, 1, handle_deleteid },
    { "delpartition", PERMISSION_ADMIN, 1, 1, handle_delpartition },
    { "disableoutput", PERMISSION_ADMIN, 1, 1, handle_disableoutput },
    { "enableoutput", PERMISSION_ADMIN, 1, 1, handle_enableoutput },
#ifdef ENABLE_DATABASE
    { "find", PERMISSION_READ, 1, -1, handle_find },
    { "findadd", PERMISSION_ADD, 1, -1, handle_findadd},
#endif
#ifdef ENABLE_CHROMAPRINT
    { "getfingerprint", PERMISSION_READ, 1, 1, handle_getfingerprint },
#endif
    { "getvol", PERMISSION_READ, 0, 0, handle_getvol },
    { "idle", PERMISSION_READ, 0, -1, handle_idle },
    { "kill", PERMISSION_ADMIN, -1, -1, handle_kill },
#ifdef ENABLE_DATABASE
    { "list", PERMISSION_READ, 1, -1, handle_list },
    { "listall", PERMISSION_READ, 0, 1, handle_listall },
    { "listallinfo", PERMISSION_READ, 0, 1, handle_listallinfo },
#endif
    { "listfiles", PERMISSION_READ, 0, 1, handle_listfiles },
#ifdef ENABLE_DATABASE
    { "listmounts", PERMISSION_READ, 0, 0, handle_listmounts },
#endif
#ifdef ENABLE_NEIGHBOR_PLUGINS
    { "listneighbors", PERMISSION_READ, 0, 0, handle_listneighbors },
#endif
    { "listpartitions", PERMISSION_READ, 0, 0, handle_listpartitions },
    { "listplaylist", PERMISSION_READ, 1, 1, handle_listplaylist },
    { "listplaylistinfo", PERMISSION_READ, 1, 1, handle_listplaylistinfo },
    { "listplaylists", PERMISSION_READ, 0, 0, handle_listplaylists },
    { "load", PERMISSION_ADD, 1, 3, handle_load },
    { "lsinfo", PERMISSION_READ, 0, 1, handle_lsinfo },
    { "mixrampdb", PERMISSION_PLAYER, 1, 1, handle_mixrampdb },
    { "mixrampdelay", PERMISSION_PLAYER, 1, 1, handle_mixrampdelay },
#ifdef ENABLE_DATABASE
    { "mount", PERMISSION_ADMIN, 2, 2, handle_mount },
#endif
    { "move", PERMISSION_PLAYER, 2, 2, handle_move },
    { "moveid", PERMISSION_PLAYER, 2, 2, handle_moveid },
    { "moveoutput", PERMISSION_ADMIN, 1, 1, handle_moveoutput },
    { "newpartition", PERMISSION_ADMIN, 1, 1, handle_newpartition },
    { "next", PERMISSION_PLAYER, 0, 0, handle_next },
    { "notcommands", PERMISSION_NONE, 0, 0, handle_not_commands },
    { "outputs", PERMISSION_READ, 0, 0, handle_devices },
    { "outputset", PERMISSION_ADMIN, 3, 3, handle_outputset },
    { "partition", PERMISSION_READ, 1, 1, handle_partition },
    { "password", PERMISSION_NONE, 1, 1, handle_password },
    { "pause", PERMISSION_PLAYER, 0, 1, handle_pause },
    { "ping", PERMISSION_NONE, 0, 0, handle_ping },
    { "play", PERMISSION_PLAYER, 0, 1, handle_play },
    { "playid", PERMISSION_PLAYER, 0, 1, handle_playid },
    { "playlist", PERMISSION_READ, 0, 0, handle_playlist },
    { "playlistadd", PERMISSION_CONTROL, 2, 3, handle_playlistadd },
    { "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
    { "playlistdelete", PERMISSION_CONTROL, 2, 2, handle_playlistdelete },
    { "playlistfind", PERMISSION_READ, 1, -1, handle_playlistfind },
    { "playlistid", PERMISSION_READ, 0, 1, handle_playlistid },
    { "playlistinfo", PERMISSION_READ, 0, 1, handle_playlistinfo },
    { "playlistmove", PERMISSION_CONTROL, 3, 3, handle_playlistmove },
    { "playlistsearch", PERMISSION_READ, 1, -1, handle_playlistsearch },
    { "plchanges", PERMISSION_READ, 1, 2, handle_plchanges },
    { "plchangesposid", PERMISSION_READ, 1, 2, handle_plchangesposid },
    { "previous", PERMISSION_PLAYER, 0, 0, handle_previous },
    { "prio", PERMISSION_PLAYER, 2, -1, handle_prio },
    { "prioid", PERMISSION_PLAYER, 2, -1, handle_prioid },
    { "random", PERMISSION_PLAYER, 1, 1, handle_random },
    { "rangeid", PERMISSION_ADD, 2, 2, handle_rangeid },
    { "readcomments", PERMISSION_READ, 1, 1, handle_read_comments },
    { "readmessages", PERMISSION_READ, 0, 0, handle_read_messages },
    { "readpicture", PERMISSION_READ, 2, 2, handle_read_picture },
    { "rename", PERMISSION_CONTROL, 2, 2, handle_rename },
    { "repeat", PERMISSION_PLAYER, 1, 1, handle_repeat },
    { "replay_gain_mode", PERMISSION_PLAYER, 1, 1,
      handle_replay_gain_mode },
    { "replay_gain_status", PERMISSION_READ, 0, 0,
      handle_replay_gain_status },
    { "rescan", PERMISSION_CONTROL, 0, 1, handle_rescan },
    { "rm", PERMISSION_CONTROL, 1, 1, handle_rm },
    { "save", PERMISSION_CONTROL, 1, 1, handle_save },
#ifdef ENABLE_DATABASE
    { "search", PERMISSION_READ, 1, -1, handle_search },
    { "searchadd", PERMISSION_ADD, 1, -1, handle_searchadd },
    { "searchaddpl", PERMISSION_CONTROL, 2, -1, handle_searchaddpl },
#endif
    { "seek", PERMISSION_PLAYER, 2, 2, handle_seek },
    { "seekcur", PERMISSION_PLAYER, 1, 1, handle_seekcur },
    { "seekid", PERMISSION_PLAYER, 2, 2, handle_seekid },
    { "sendmessage", PERMISSION_CONTROL, 2, 2, handle_send_message },
    { "setvol", PERMISSION_PLAYER, 1, 1, handle_setvol },
    { "shuffle", PERMISSION_PLAYER, 0, 1, handle_shuffle },
    { "single", PERMISSION_PLAYER, 1, 1, handle_single },
    { "stats", PERMISSION_READ, 0, 0, handle_stats },
    { "status", PERMISSION_READ, 0, 0, handle_status },
#ifdef ENABLE_SQLITE
    { "sticker", PERMISSION_ADMIN, 3, -1, handle_sticker },
#endif
    { "stop", PERMISSION_PLAYER, 0, 0, handle_stop },
    { "subscribe", PERMISSION_READ, 1, 1, handle_subscribe },
    { "swap", PERMISSION_PLAYER, 2, 2, handle_swap },
    { "swapid", PERMISSION_PLAYER, 2, 2, handle_swapid },
    { "tagtypes", PERMISSION_NONE, 0, -1, handle_tagtypes },
    { "toggleoutput", PERMISSION_ADMIN, 1, 1, handle_toggleoutput },
#ifdef ENABLE_DATABASE
    { "unmount", PERMISSION_ADMIN, 1, 1, handle_unmount },
#endif
    { "unsubscribe", PERMISSION_READ, 1, 1, handle_unsubscribe },
    { "update", PERMISSION_CONTROL, 0, 1, handle_update },
    { "urlhandlers", PERMISSION_READ, 0, 0, handle_urlhandlers },
    { "volume", PERMISSION_PLAYER, 1, 1, handle_volume },
};

命令还是挺多的,我们记住常用的几个,基本能满足开发需求。如 

listall——列出目录下所有音乐文件

play——播放

pause——暂停

stop——停止播放

prev——上一首

next——下一首

listall——列出所有音频文件

另外,还有几个命令也可能会用到:

volume——用来调整音量。硬件控制模式下无效,除非改为软件控制模式:software,在mpd.conf文件中配置 audio-output节点下的 mixer_type。

audio_output {
        type            "alsa"
        name            "My ALSA Device"
        device          "default"       # optional
        mixer_type      "software"      # optional
#       mixer_device    "default"       # optional
#       mixer_control   "PCM"           # optional
#       mixer_index     "0"             # optional
}

不过这个配置不一定有用,有些声卡配置了也控制不到音量的,原因不明。

音量的设置格式为 +/- xx%,就是加或减掉多少百分比的音量。例如,要增大 20%,那就发送 volume +20;要减小 10% 就发送 volume -10。当然,是可以指定确定的值,如50%,发送 volume 50。

seek——设置播放进度,可以用时间,也可以用百分比。如定位到歌曲的 70% 处,发送 seek 70%;要定位到 1分 12 秒处,发送 seek 00:01:12。

current——显示当前正在播放的曲目。

-------------------------------------------------------------------------------------

有了上面的理论基础,相信你现在已经会写程序了。咱们用一个 Win Forms 程序为例,实现一个最简单的功能,列出音乐目录下的所有曲目,即用 listall 命令。

 上面两个文本框,名为 tbServer 的用来输入服务器地址;名为 tbPort 的用来输入端口号。“连接”按钮名为 btnConnt,单击后连接服务器。

namespace TestApp
{
    using System.Net;
    using System.Net.Sockets;
    // 这里直接导入静态成员
    using static System.Text.Encoding;

    public partial class Form1 : Form
    {
        TcpClient mpCl;
        public Form1()
        {
            InitializeComponent();

            // 实例化TcpClient对象
            mpCl = new(AddressFamily.InterNetwork);
            // 清理TcpClient对象
            this.FormClosing += (_, _) =>
            {
                mpCl?.Close();
                mpCl?.Dispose();
/            };
        }
    }
}

此处使用比较简便的 TcpClient 类。

处理“连接”按钮的 Click 事件,尝试连接 MPD 服务。

        private void btnConnt_Click(object sender, EventArgs e)
        {
            // 若已连接,不再往下执行
            if(mpCl.Connected)
            {
                MessageBox.Show("亲,你想干吗?这不是已经连接了吗。");
                return;
            }
            string host = tbServer.Text.Trim();
            if(string.IsNullOrEmpty(host))
            {
                MessageBox.Show("尼马,你不提供远程服务器地址,连接个鬼啊。");
                return;
            }
            if(!int.TryParse(tbPort.Text.Trim(), out int port))
            {
                port = 6600;    // 用默认值
            }
            // 开始连接
            try
            {
                mpCl.Connect(host, port);
                MessageBox.Show("全球华人发来贺电,连接成功。");
            }
            catch
            {
                MessageBox.Show("连接失败,请优化人品后再试。");
            }
        }

接下来窗口上放一个按钮,名为 btnList,单击后列出所有曲目,并显示在 ListBox 控件(Name = lsbSongs)中。

        private void btnList_Click(object sender, EventArgs e)
        {
            // 检查连接没有?
            if(!mpCl.Connected)
            {
                return;
            }
            lsbSongs.Items.Clear();

            using StreamWriter sw = new(mpCl.GetStream(),
                                  encoding: ASCII,
                                  leaveOpen: true);
            // 换行符要用 \n
            sw.NewLine = "\n";
            // 命令(结尾不要带\n因为WriteLine会自动加上)
            string command = "listall";
            // 发送
            sw.WriteLine(command);
            sw.Flush();     // 这一行必须

            // 接收服务器回传的内容
            using StreamReader sr = new(
                    stream: mpCl.GetStream(),
                    encoding: UTF8, //这里要用UTF-8编码
                    leaveOpen: true
                );
            // 一行一行地读比较快,一次性全读完会很卡
            // 因为网络流传过来的文本没有给定EOF,只有等待超时才返回
            string line;
            // a、读首行,以 OK 开头,后跟MPD <版本号>
            line = sr.ReadLine();
            if (line == null) return;
            if(line.StartsWith("OK"))
            {
                // 接下来是曲目,每行一首
                // 所有曲目发送完后,会有一行“OK”
                while((line = sr.ReadLine()) != "OK")
                {
                    lsbSongs.Items.Add(line);
                }
            }
        }

为了方便 WriteLine 和 ReadLine,咱们用 StreamWriter 和 StreamReader 类。

这里有几点必须注意,很重要:

1、Writer 的编码要使用 ASCII,不要用 UTF8,否则MPD会回复无效字母,从 MPD 的源代码分析,它在处理命令时,会检测 ASCII 字符。

Tokenizer::NextWord()
{
    char *const word = input;

    if (*input == 0)
        return nullptr;

    /* check the first character */

    if (!valid_word_first_char(*input))
        throw std::runtime_error("Letter expected");

    /* now iterate over the other characters until we find a
       whitespace or end-of-string */

    while (*++input != 0) {
        if (IsWhitespaceFast(*input)) {
            /* a whitespace: the word ends here */
            *input = 0;
            /* skip all following spaces, too */
            input = StripLeft(input + 1);
            break;
        }

        if (!valid_word_char(*input))
            throw std::runtime_error("Invalid word character");
    }

    /* end of string: the string is already null-terminated
       here */

    return word;
}

2、设置 NewLine 属性为\n,防止使用 \r\n。

3、写完后要调用 Flush 方法,这样命令才会真正发送。

4、MPD 回应的消息为文本,第一行以 OK (大写)开头,然后是 MPD + 版本号,这个可忽略不管,只看有OK开头就行。

5、接着是发曲目,格式为 file: 文件名,一行一条记录。

6、所有东西发完后,会发一条OK。

好,运行看看效果。

 这个程序仅作演示,其实有 bug,正确做法应该是每次发命令时再连接,发完命令接收完消息后断开,不应该一直占用连接。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK