made music player thingy with tui
sophuwu sophie@sophuwu.com
Sun, 07 Sep 2025 12:48:52 +0200
M
go.mod
→
go.mod
@@ -2,15 +2,17 @@ module melgody
go 1.23.0 -require github.com/faiface/beep v1.1.0 +require ( + github.com/faiface/beep v1.1.0 + golang.org/x/term v0.34.0 +) require ( github.com/hajimehoshi/go-mp3 v0.3.0 // indirect github.com/hajimehoshi/oto v0.7.1 // indirect github.com/pkg/errors v0.9.1 // indirect golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect - golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect + golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect )
M
go.sum
→
go.sum
@@ -26,21 +26,16 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
M
main.go
→
main.go
@@ -5,6 +5,7 @@ "fmt"
"time" "errors" + "github.com/faiface/beep" "github.com/faiface/beep/mp3" "github.com/faiface/beep/speaker"@@ -115,7 +116,7 @@ Shuffle: shuffle,
Repeat: repeat, Source: pl, }, - nx: make(chan struct{}), + nx: true, end: make(chan struct{}), } return player@@ -135,11 +136,13 @@ return &next
} type Player struct { - Q *Queue - Np *MusicFile - skip func() - ctrl beep.Ctrl - nx, end chan struct{} + Q *Queue + Np *MusicFile + Progress func() (string, float64) + skip func() + ctrl beep.Ctrl + end chan struct{} + nx bool } func (p *Player) TogglePause() {@@ -157,11 +160,12 @@ }
} func (p *Player) play(song *MusicFile) (err error) { - speaker.Clear() wait := make(chan struct{}) var streamer beep.StreamSeekCloser var format beep.Format + var s string + var D float64 file, err := os.Open(song.Path) if err != nil {@@ -173,6 +177,14 @@ if err != nil {
goto retOne } + D = float64(streamer.Len() * format.Precision / int(format.SampleRate) / format.NumChannels) + s = fmt.Sprintf("%2.2d:%2.2d", int(D)/60, int(D)%60) + p.Progress = func() (string, float64) { + d := float64(D) * float64(streamer.Position()) / float64(streamer.Len()) + dd := int(d) + return fmt.Sprintf("%2.2d:%2.2d/%s", dd/60, dd%60, s), float64(d) / D + } + err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) if err != nil { goto retTwo@@ -198,7 +210,7 @@ ret:
return err } -func (p *Player) Play(nx chan struct{}) (err error) { +func (p *Player) Play() (err error) { p.Q.Fill() p.end = make(chan struct{}) for {@@ -206,13 +218,12 @@ p.Np = p.Q.Next()
if p.Np == nil { break } - nx <- struct{}{} err = p.play(p.Np) if err != nil { return err } } - close(nx) + p.nx = false p.end <- struct{}{} close(p.end) return nil@@ -264,42 +275,68 @@ if n, er = os.Stdin.Read(buf); er != nil {
safeExit(1) } switch string(buf[:n]) { - case "q", "Q", "\x03", "\x04": + case "Q", "\x03", "\x04": player.Quit() case " ": player.TogglePause() case "s", "S": + player.Q.Shuffle = !player.Q.Shuffle + player.Q.Fill() + case "r", "R": + player.Q.Repeat = !player.Q.Repeat + case "N": player.Skip() } } }() - nx := make(chan struct{}) go func() { - for ok := true; ok; _, ok = <-nx { - fmt.Print("\033[2J\033[1;1H") // Clear screen - fmt.Print("Controls: [space] Pause/Play | [s] Skip | [q] Quit\n\r") - if player.Np == nil { - fmt.Print("No song is currently playing.\n\r") + var i int + var s, tstr string + var prog float64 + var song MusicFile + var W, H int + for player.nx { + time.Sleep(200 * time.Millisecond) + W, H, _ = term.GetSize(int(os.Stdin.Fd())) + fmt.Printf("\033[2J\033[%d;1H", H-6) // Clear screen + if player.Np != nil { + fmt.Printf(" Now Playing: %s\n\r", player.Np.Name) + tstr, prog = player.Progress() + i = int(prog * float64(W-21)) + fmt.Printf(" %s [%sO%s] %s\n\r", map[bool]string{true: "||", false: "▶ "}[player.ctrl.Paused], strings.Repeat("=", i), strings.Repeat("-", W-22-i), tstr) } else { - fmt.Printf("Now Playing: %s\n\r", player.Np.Name) + fmt.Printf(" Now Playing: (none)\n\r") + fmt.Printf(" || [%s] 00:00/00:00\n\r", strings.Repeat("-", W-21)) } - fmt.Print("Up Next:\n\r") + fmt.Printf("\n") + fmt.Printf(" [SPACE] Pause/Play | Status: %8.8s\n\r", map[bool]string{true: "paused", false: "playing"}[player.ctrl.Paused]) + fmt.Printf(" [SHIFT]+[Q] Quit | [SHIFT]+[N] Next\n\r") + mp := map[bool]string{true: "On ", false: "Off"} + fmt.Printf(" [S] Shuffle: %s | [R] Repeat: %s\n\r", mp[player.Q.Shuffle], mp[player.Q.Repeat]) + + s = "" if len(player.Q.UpNext) == 0 { - fmt.Print(" (queue is empty)\n\r") - continue + s = " (empty)" } - for i, song := range player.Q.UpNext { - if i >= 5 { - fmt.Printf(" ...and %d more\n\r", len(player.Q.UpNext)-i) + fmt.Printf("\033[%d;1H Queue %s\n\r", H-9, s) + for i, song = range player.Q.UpNext { + if H-10-i <= 2 { + fmt.Printf("\033[%d;1H %d more\n\r", H-10-i, len(player.Q.UpNext)-i) break } - fmt.Printf(" %d. %s\n\r", i+1, song.Name) + fmt.Printf("\033[%d;1H%5d - %s\n\r", H-10-i, i+1, func(s string) string { + if len(s) > W-10 { + return s[:W-13] + "..." + } + return s + }(song.Name)) } + } }() - err = player.Play(nx) + err = player.Play() if err != nil { fmt.Println("An error occured during playback:", err.Error()) safeExit(1)