实现代码
界面定义
type AppGUI struct {
baseDir string // 文件目录
songs []string // 歌曲集合
curSong *MusicEntry // 当前歌曲
currentSongName *widget.Label // 当前曲目名称
progress *widget.ProgressBar // 播放进度
consumedTime *widget.Label // 已用时间
remainedTime *widget.Label // 剩余时间
playBtn *widget.Button // 播放
paused bool // 是否暂停标志
nextBtn *widget.Button // 下一首
preBtn *widget.Button // 上一首
forwardBtn *widget.Button // 快进
backwardBtn *widget.Button // 快退
songIdx int // 当前歌曲序号
appDir string // 程序运行目录
newSongFlag bool // 新的一首歌
endUpdateProgress chan bool // 停止更新进度条
}
代码说明
界面布局首先在最外层使用了单列的GridLayout,所有Widget将会由上往下放置,第一行放置歌曲名,第二行放軒三个元素:已播放时间,进度条,歌曲总时长,第三行放置歌曲的控制按钮。
这里要注意的是第二行采有的是BorderLayout,使已播放时间、歌曲总时长这两个Label分别置于左右两端,而余下的空间全部由ProgressBar占据。
第三行的主要按键功能实现的说明如下:
播放与暂停功能
这里主要是要将beep.Ctrl和beep.StreamSeekCloser用好,beep.Ctrl中的Paused变量表示是否暂停流,appui.curSong.Format.SampleRate.D(appui.curSong.Streamer.Position()).Round(time.Second).String()可以计算得到当前已经播放的时长。
上一首/下一首功能
这两个按钮的功能类似,主要是通过通道使AppGUI.PlaySong()启动的两个协程退出,同时还原相关参数状态;这里要注意的是MusicEntry.Play()结束时要调用speaker.Clear(),将流清空,否则下次调用speaker.Init()会形成死锁。
界面代码
package musicplayer
import (
"fmt"
"fyne.io/fyne"
"fyne.io/fyne/app"
"fyne.io/fyne/layout"
"fyne.io/fyne/widget"
"io/ioutil"
"os"
"path"
"path/filepath"
"time"
)
type AppGUI struct {
baseDir string // 文件目录
songs []string // 歌曲集合
curSong *MusicEntry // 当前歌曲
currentSongName *widget.Label // 当前曲目名称
progress *widget.ProgressBar // 播放进度
consumedTime *widget.Label // 已用时间
remainedTime *widget.Label // 剩余时间
playBtn *widget.Button // 播放
paused bool // 是否暂停标志
nextBtn *widget.Button // 下一首
preBtn *widget.Button // 上一首
forwardBtn *widget.Button // 快进
backwardBtn *widget.Button // 快退
songIdx int // 当前歌曲序号
appDir string // 程序运行目录
newSongFlag bool // 新的一首歌
endUpdateProgress chan bool // 停止更新进度条
}
func (appui *AppGUI) Run() {
a := app.New()
appui.newSongFlag = true
appui.songIdx = 0
re, _ := os.Executable()
appui.appDir = filepath.Dir(re)
fmt.Println("pwd:" + appui.appDir)
appui.songs = make([]string, 0, 10)
appui.endUpdateProgress = make(chan bool)
appui.baseDir = "music_res"
appui.currentSongName = widget.NewLabel("--")
appui.progress = widget.NewProgressBar()
appui.consumedTime = widget.NewLabel("0")
appui.remainedTime = widget.NewLabel("0")
appui.playBtn = widget.NewButton("Play", appui.PlaySong)
appui.paused = true
appui.nextBtn = widget.NewButton("Next", appui.NextSong)
appui.preBtn = widget.NewButton("Prev", appui.PrevSong)
appui.forwardBtn = widget.NewButton("Forward", nil)
appui.backwardBtn = widget.NewButton("Backward", nil)
appui.progress.Min = 0
appui.progress.Max = 100
appui.progress.SetValue(0)
files, _ := ioutil.ReadDir(appui.appDir + "/" + appui.baseDir)
for _, onefile := range files {
if onefile.IsDir() {
// do nothing
} else {
// 放入曲库
postfix := path.Ext(onefile.Name())
if postfix == ".mp3" {
appui.songs = append(appui.songs, onefile.Name())
}
}
}
// 显示第一首歌的名字
if len(appui.songs) != 0 {
appui.currentSongName.SetText(appui.songs[0])
}
w := a.NewWindow("MP3播放器")
w.SetTitle("MP3 Player")
w.SetContent(fyne.NewContainerWithLayout(layout.NewGridLayout(1),
appui.currentSongName,
fyne.NewContainerWithLayout(layout.NewBorderLayout(nil, nil, appui.consumedTime, appui.remainedTime),
appui.consumedTime,
appui.remainedTime,
appui.progress,
),
fyne.NewContainerWithLayout(layout.NewGridLayout(5),
appui.preBtn,
appui.backwardBtn,
appui.playBtn,
appui.forwardBtn,
appui.nextBtn,
),
))
appui.curSong = &MusicEntry{}
if len(appui.songs) != 0 {
appui.curSong.Source = appui.appDir + "/" + appui.baseDir + "/" + appui.songs[appui.songIdx]
}
w.ShowAndRun()
}
// hooks
func (appui *AppGUI) PlaySong() {
if appui.newSongFlag {
appui.newSongFlag = false
appui.curSong.Open()
appui.remainedTime.SetText(appui.curSong.Format.SampleRate.D(appui.curSong.Streamer.Len()).Round(time.Second).String())
// 播放音乐
go appui.curSong.Play()
// 更新进度条
go appui.UpdateProcess()
}
if appui.paused == true {
appui.playBtn.SetText("Pause")
appui.paused = false
appui.curSong.paused <- false
} else {
appui.playBtn.SetText("Play")
appui.paused = true
appui.curSong.paused <- true
}
}
func (appui *AppGUI) UpdateProcess() {
appui.progress.Min = 0
appui.progress.Max = float64(appui.curSong.Streamer.Len())
for {
select {
case <-appui.endUpdateProgress:
return
case <-time.After(time.Second):
appui.progress.SetValue(appui.curSong.progress)
appui.consumedTime.SetText(appui.curSong.Format.SampleRate.D(appui.curSong.Streamer.Position()).Round(time.Second).String())
}
}
}
func (appui *AppGUI) NextSong() {
appui.songIdx = appui.songIdx + 1
if appui.songIdx >= len(appui.songs) {
appui.songIdx = 0
}
appui.Reset()
}
func (appui *AppGUI) PrevSong() {
appui.songIdx = appui.songIdx - 1
if appui.songIdx < 0 {
appui.songIdx = len(appui.songs) - 1
}
appui.Reset()
}
func (appui *AppGUI) Reset() {
appui.currentSongName.SetText(appui.songs[appui.songIdx])
appui.curSong.Source = appui.appDir + "/" + appui.baseDir + "/" + appui.songs[appui.songIdx]
appui.paused = true
appui.playBtn.SetText("Play")
if !appui.newSongFlag {
appui.curSong.Stop()
appui.endUpdateProgress <- true
}
appui.newSongFlag = true
}
播放控制代码
package musicplayer
import (
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"log"
"os"
"time"
)
type MusicEntry struct {
Id string // 编号
Name string // 歌名
Artist string // 作者
Source string // 位置
Type string // 类型
Filestream *os.File // 文件流
Format beep.Format // 文件信息
Streamer beep.StreamSeekCloser // 流信息
done chan bool // 结束信号
ctrl *beep.Ctrl // 控制器
paused chan bool // 暂停标志
progress float64 // 进度值
}
func (me *MusicEntry) Open() {
var err error
me.Filestream, err = os.Open(me.Source)
if err != nil {
log.Fatal(err)
}
me.Streamer, me.Format, err = mp3.Decode(me.Filestream)
if err != nil {
log.Fatal(err)
}
speaker.Init(me.Format.SampleRate, me.Format.SampleRate.N(time.Second/10))
me.done = make(chan bool)
me.paused = make(chan bool)
me.ctrl = &beep.Ctrl{Streamer: beep.Seq(me.Streamer, beep.Callback(func() {
me.done <- true
})), Paused: false}
}
func (me *MusicEntry) Play() {
defer me.Streamer.Close()
speaker.Play(me.ctrl)
for {
select {
case <-me.done:
// 此处必须调用,否则下次Init会有死锁
speaker.Clear()
return
case value := <-me.paused:
speaker.Lock()
me.ctrl.Paused = value
speaker.Unlock()
case <-time.After(time.Second):
speaker.Lock()
me.progress = float64(me.Streamer.Position())
speaker.Unlock()
}
}
}
func (me *MusicEntry) Stop() {
select {
case me.done <- true:
default:
}
}