先总体对界面进行定义如下:
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 // 停止更新进度条 }
目前已经实现的功能有:MP3文件的切换,播放/暂停,上一首,下一首。
总体界面运行效果如下图,由于Fyne没有原生的文件选择器,所以这里使用的固定目录存放Mp3文件,使用的时候需要将Mp3文件放置在与应用程序同级的music_res目录中,程序启动时会自动识别该目录下的Mp3文件:
界面布局首先在最外层使用了单列的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()会形成死锁。
主要代码如下:
gui.go
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 }
mplayer.go
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: } }