golang fyne 简单的音乐播放器

实现代码

界面定义

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           // 停止更新进度条
}

2023-07-17T01:45:39.png

代码说明

界面布局首先在最外层使用了单列的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:
    }
}