先总体对界面进行定义如下:

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:
   }
}