树莓派的日常家居玩法多多,制作一台属于自己的数字音乐播放机是其中的一种。严格上说,树莓派是没有声卡的,其板载的 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 pi@192.168.0.106:/home/pi/music

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

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

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

好了,基本的测试条件已满足,下面老周再告诉你怎么通过编程来控制 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,正确做法应该是每次发命令时再连接,发完命令接收完消息后断开,不应该一直占用连接。

【.NET 与树莓派】用 MPD 制作数字音乐播放器的更多相关文章

  1. 吴裕雄--天生自然python学习笔记:python 用pygame模块制作 MP3 音乐播放器

    利用 music 对象来制作一个 MP3 音乐播放器 . 应用程序总览 从歌曲清单中选择指定的歌曲,单击“播放”按钮可开始播放, 在播放 xxx 歌曲”的信息. 歌曲播放的过程中,可以暂停.停止,也可 ...

  2. 使用LM386制作Arduino音乐播放器

    在我们的项目中添加声音或音乐总是会使其看起来更酷一些,听上去更有吸引力.特别是如果您使用的是Arduino开发板,并且有很多空余的引脚,只需要添加一个SD卡模块和一个普通的扬声器即可轻松添加音效.在本 ...

  3. 【大结局】《从案例中学习JavaScript》之酷炫音乐播放器(四)

    这是之前写的用H5制作的音乐播放器,前三节其实已经做得差不多了,音轨的制作原理已经在上一节说明,不过一直还没有和音乐对接. 本章作为该系列的一个完结篇,我会专门把动态音轨的实现代码贴出来,demo地址 ...

  4. 使用react native制作的一款网络音乐播放器

    使用react native制作的一款网络音乐播放器 基于第三方库 react-native-video设计"react-native-video": "^1.0.0&q ...

  5. HTML+纯JS制作音乐播放器

    该篇文章会教你通过JavaScript制作一个简单的音乐播放器.包括播放.暂停.上一曲和下一曲. 阅读本文章你需要对HTML.CSS和Javascript有基本的了解. 话不多说,先上图. emmm. ...

  6. 使用 原生js 制作插件 (javaScript音乐播放器)

    1.引用页面 index.html <!DOCTYPE html> <html lang="en"> <head> <meta chars ...

  7. 计算机应用第七次作业 html制作个人音乐播放站点

    计算机应用第七次作业 html制作个人音乐播放站点 请访问下边网址查看具体操作: http://www.cnblogs.com/qingyundian/p/7878892.html

  8. Swift - 制作一个在线流媒体音乐播放器(使用StreamingKit库)

    在之前的文章中,我介绍了如何使用 AVPlayer 制作一个简单的音乐播放器(点击查看1.点击查看2).虽然这个播放器也可以播放网络音频,但其实际上是将音频文件下载到本地后再播放的. 本文演示如何使用 ...

  9. Android 音视频深入 十三 OpenSL ES 制作音乐播放器,能暂停和调整音量(附源码下载)

    项目地址https://github.com/979451341/OpenSLAudio OpenSL ES 是基于NDK也就是c语言的底层开发音频的公开API,通过使用它能够做到标准化, 高性能,低 ...

随机推荐

  1. 学会了这些英文单词,妈妈再也不用担心我学不会Python

    前言   很多转行或刚入行做测试的小伙伴学习Python时,经常会问一句话:我英语不好能不能学会代码. 答案是:肯定的!你如果英语好学开发语言肯定要比不会英语的小伙伴学起来.当代码报错时全是英文,毕竟 ...

  2. HTML[2种特殊选择器]_伪类选择器&属性选择器

    本文介绍两种特殊的选择器 1.伪类选择器 2.属性选择器 1.伪类选择器 ...: nth-of -type (x) x为同类型兄弟元素中的排名 例如: <body> <ul> ...

  3. 讲师征集| .NET Conf China 2021正式启动

    最近社区小伙伴们一直在为11月即将在武汉举办的 第三届.NET中国峰会而忙碌,社区活动官网设计和开发工作还在进行,我们在国庆节的前一天晚上向社区正式启动了活动的序幕,也就是我们确定好了举办地点.时间, ...

  4. Postman快速入门

        Postman是一款非常流行的支持HTTP/HTTPS协议的接口调试与测试工具,其功能非常强大,易用. 1 基础知识 1.1 下载与安装     Postman的安装步骤,本例以Windows ...

  5. Network Analyst Tools(Network Analyst 工具)

    Network Analyst 工具 1.分析 # Process: 创建 OD 成本矩阵图层 arcpy.MakeODCostMatrixLayer_na("", "O ...

  6. 题解 [HNOI2019]序列

    题目传送门 题目大意 给出一个\(n\)个数的数列\(A_{1,2,...,n}\),求出一个单调不减的数列\(B_{1,2,...,n}\),使得\(\sum_{i=1}^{n}(A_i-B_i)^ ...

  7. pycharm上的python虚拟环境移到离线机器上

    Pycharm的Terminal 中执行: 查看现有的包到requirements.txt中 pip freeze > requirements.txt 生成依赖包 D:\machangwei\ ...

  8. ArrayList和LinkedList、及Vector对比分析

    ArrayList和LinkedList 底层结构 两者的差别主要来自于底层的数据结构不同,ArrayList是基于数组实现的,LinkedList是基于双链表实现的. 接口实现 LinkedList ...

  9. SpringBoot配置文件-多环境切换

    profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境: 多个文件-配置多环境: 需要多个配置文件,文件名可以是 application-{prof ...

  10. python中冒泡排序代码实现

    1.冒泡排序代码如下图: #冒泡算法l=[12,4,56,10,6,2]for i in range(0,6): for j in range(i+1,6): if l[i]>l[j]: a=l ...