【.NET 與樹莓派】用 MPD 製作數字音樂播放器

東邪獨孤發表於2021-11-09

樹莓派的日常家居玩法多多,製作一臺屬於自己的數字音樂播放機是其中的一種。嚴格上說,樹莓派是沒有音效卡的,其板載的 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,正確做法應該是每次發命令時再連線,發完命令接收完訊息後斷開,不應該一直佔用連線。

 

相關文章