mpg123

日常生活中,我们都是使用网易云音乐之类的音乐软件听歌,但是你有没有想过,作为程序员,该如何使用命令行来听(zhuang)歌(bi)呢?今天就让我们来实现这个功能吧。

写代码,一般需要搞清楚三点,输入,输出和算法。如果使用命令行听歌,输入很明确,肯定是音频数据流,数据一般可以通过网上获取,那么输出呢,肯定就是我们耳朵中听到的声音,这两点都比较简单。关键是处理数据和输出数据的算法,之前并没有怎么搞过音乐格式编码和解码之类的东西,不过不要慌,已经有强大的 mpg123 这个命令行工具帮我们搞定这类事情了。

mpg123 需要额外安装,macOS 用户可以使用 homebrew 进行安装。mpg123 可以帮助我们处理 MPEG 1.0/2.0/2.5 格式的数据流,并使用系统默认的音屏设备播放。

1
$ mpg123 [ options ] file-or-URL...

上面是它的简单使用,[options] 代表额外选项,真正的参数可以是本地音频文件地址或者 URL 地址。一般来说 file/URL 都需要是 MPEG 格式的音频比特流( audio bit stream )。

问题的关键解决之后,下面就可以利用它写一个简单的音乐播放器了,这里播放器具有两种状态,StoppedPlaying ,播放器的核心操作依赖 mpg123 实现,exec.Cmd 用于代表这个命令。除此之外,还需要指定输入和输出,输入( io.ReadCloser )可以来自 HTTP 得到的数据流,而输出( io.WriteCloser )则是该命令的输入管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type State int

const (
Stopped State = iota
Playing
)

type Player struct {
state State
currentURL string
mpg123 *exec.Cmd
src io.ReadCloser
dst io.WriteCloser
}

因为播放音乐需要 mpg123 这个依赖,所以,在程序运行时需要检查其是否存在,exec.LookPath 会在系统路径( $PATH )下面寻找,如果不存在,则返回 error

1
2
3
4
5
6
7
8
func NewPlayer() (*Player, error) {
_, err := exec.LookPath("mpg123")
if err != nil {
return nil, err
}
p := &Player{}
return p, nil
}

使用命令行听歌的关键还是在于对数据流的处理,通过 io.Copy 方法将数据流进行重定向。

1
2
3
4
5
func stream(dst io.WriteCloser, src io.ReadCloser) {
defer dst.Close()
defer src.Close()
io.Copy(dst, src)
}

因为 mpg123 需要读取音频数据流,在构造命令时,我们使用 - 来指定mpg123 从标准输入中获取数据( exec.Command("mpg123", "-q", "-") ),通过 StdinPipe() 方法可以获得连接到此命令的标准输入管道。输入的数据流可以利用 http.Get 获取,然后再利用上述 stream 方法将数据流重定向到 mpg123 的标准输入( stdin )中去,这样就可以实现音乐播放了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func (p *Player) Play(URL string) (err error) {
if p.state == Playing {
p.Stop()
return p.Play(URL)
}

mpg123 := exec.Command("mpg123", "-q", "-")
stdin, err := mpg123.StdinPipe()
if err != nil {
stdin.Close()
return
}

err = mpg123.Start()
if err != nil {
stdin.Close()
}

response, err := http.Get(URL)
if err != nil {
return
}

p.state = Playing
p.currentURL = URL
p.mpg123 = mpg123
p.src = response.Body
p.dst = stdin

go stream(p.dst, p.src)
return
}

一个音乐播放器自然少不了暂停操作。因为输入和输出分别是 io.ReadCloserio.WriteCloser 类型,所以都可以调用 Close 方法来将它们关闭。除此之外还需要将对应的 mpg123 的进程停掉( p.mpg123.Process.Kill() )。

1
2
3
4
5
6
7
8
9
10
11
12
13
func (p *Player) Stop() {
if p.state == Stopped {
return
}
p.state = Stopped
p.src.Close()
p.src = nil

p.dst.Close()
p.mpg123.Process.Kill()
p.mpg123.Wait()
p.mpg123.Handler = nil
}

剩下最后一个问题就是如何获取各大平台音乐的链接地址,下面给获取网易云音乐歌曲外链地址的方法,这里的参数 id 表示音乐的 id ,其他平台获取方法也应该类似,感兴趣的读者可以自行寻找。

1
2
3
4
5
func getMusicLink(id int) string {
s := "http://music.163.com/song/media/outer/url?id=%d.mp3"
addr = fmt.Sprintf(s, id)
return addr
}
Pieces of Valuable Programming Knowledges