summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxengineering <me@xengineering.eu>2024-09-29 12:38:30 +0200
committerxengineering <me@xengineering.eu>2024-09-29 12:38:30 +0200
commit9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309 (patch)
tree246c044d7cc9ecab9941c0419d82447a6749ce1a
parent58e6db7eca97e4514df82464cc2a1fe8d49c1adc (diff)
downloadsoundbox-app-9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309.tar
soundbox-app-9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309.tar.zst
soundbox-app-9d3e0c0f2e30dc43d1e3a1bb316d6af918bd8309.zip
Add web radio player app as MVP
While streaming to soundbox devices is not supported this MVP is a working mpv-based GUI to play sound from web radio URLs on a Linux computer.
-rw-r--r--go.mod16
-rw-r--r--go.sum23
-rw-r--r--main.go157
3 files changed, 196 insertions, 0 deletions
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..01240cb
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,16 @@
+module xengineering.eu/soundbox
+
+go 1.22.5
+
+require gioui.org v0.7.1
+
+require (
+ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
+ gioui.org/shader v1.0.8 // indirect
+ github.com/go-text/typesetting v0.1.1 // indirect
+ golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
+ golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect
+ golang.org/x/image v0.18.0 // indirect
+ golang.org/x/sys v0.22.0 // indirect
+ golang.org/x/text v0.16.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..94dae7e
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,23 @@
+eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
+eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
+gioui.org v0.7.1 h1:l7OVj47n1z8acaszQ6Wlu+Rxme+HqF3q8b+Fs68+x3w=
+gioui.org v0.7.1/go.mod h1:5Kw/q7R1BWc5MKStuTNvhCgSrRqbfHc9Dzfjs4IGgZo=
+gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
+gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
+gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
+gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
+gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
+github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
+github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
+github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
+github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
+golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
+golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
+golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
+golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
+golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..5701c72
--- /dev/null
+++ b/main.go
@@ -0,0 +1,157 @@
+package main
+
+import (
+ "context"
+ "image/color"
+ "log"
+ "os"
+ "os/exec"
+ "sync"
+
+ "gioui.org/app"
+ "gioui.org/layout"
+ "gioui.org/op"
+ "gioui.org/text"
+ "gioui.org/unit"
+ "gioui.org/widget"
+ "gioui.org/widget/material"
+)
+
+func main() {
+ ui := NewUi()
+ go func() {
+ err := ui.Run()
+ if err != nil {
+ log.Fatal(err)
+ }
+ os.Exit(0)
+ }()
+ app.Main()
+}
+
+type State struct {
+ sync.Mutex
+ Theme *material.Theme
+ Title string
+ UrlEditor widget.Editor
+ PlayPauseButton widget.Clickable
+ PlayPauseButtonText string
+ PlayerContext context.Context
+ PlayerCancel context.CancelFunc
+}
+
+type Ui struct {
+ Window *app.Window
+ State State
+}
+
+func NewUi() *Ui {
+ ui := Ui{}
+
+ ui.State.Theme = material.NewTheme()
+
+ ui.Window = new(app.Window)
+ ui.Window.Option(app.Title("soundbox app"))
+
+ ui.State.Title = "soundbox"
+ ui.State.PlayPauseButtonText = "Play"
+
+ return &ui
+}
+
+func (ui *Ui) Cleanup() {
+ ui.State.Lock()
+ defer ui.State.Unlock()
+
+ if ui.State.PlayerCancel != nil {
+ ui.State.PlayerCancel()
+ }
+}
+
+func (ui *Ui) Run() error {
+ defer ui.Cleanup()
+ var ops op.Ops
+ for {
+ switch e := ui.Window.Event().(type) {
+ case app.DestroyEvent:
+ return e.Err
+ case app.FrameEvent:
+ gtx := app.NewContext(&ops, e)
+ ui.HandleInputs(gtx)
+ ui.Layout(gtx)
+ e.Frame(gtx.Ops)
+ }
+ }
+}
+
+func (ui *Ui) HandleInputs(gtx layout.Context) {
+ ui.State.Lock()
+ defer ui.State.Unlock()
+
+ if ui.State.PlayPauseButton.Clicked(gtx) {
+ if ui.State.UrlEditor.ReadOnly {
+ ui.State.PlayerCancel()
+ } else {
+ ui.State.PlayerContext, ui.State.PlayerCancel = context.WithCancel(context.Background())
+ go mpv(ui.State.PlayerContext, ui.State.UrlEditor.Text(), ui)
+ }
+ }
+}
+
+func (ui *Ui) Layout(gtx layout.Context) layout.Dimensions {
+ ui.State.Lock()
+ defer ui.State.Unlock()
+
+ inset := layout.UniformInset(unit.Dp(10))
+
+ flex := layout.Flex{
+ Axis: layout.Vertical,
+ Spacing: layout.SpaceEnd,
+ }
+
+ h1 := material.H1(ui.State.Theme, ui.State.Title)
+ h1.Color = color.NRGBA{R: 88, G: 88, B: 88, A: 255}
+ h1.Alignment = text.Middle
+
+ editor := material.Editor(ui.State.Theme, &ui.State.UrlEditor, "Audio stream URL")
+ editor.Editor.Alignment = text.Middle
+
+ button := material.Button(ui.State.Theme, &ui.State.PlayPauseButton, ui.State.PlayPauseButtonText)
+
+ return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return flex.Layout(gtx,
+ layout.Rigid(h1.Layout),
+ layout.Rigid(layout.Spacer{Height: unit.Dp(25)}.Layout),
+ layout.Rigid(editor.Layout),
+ layout.Rigid(layout.Spacer{Height: unit.Dp(25)}.Layout),
+ layout.Rigid(button.Layout),
+ )
+ })
+}
+
+func mpv(ctx context.Context, url string, ui *Ui) {
+ setPlayingState := func(isPlaying bool) {
+ ui.State.Lock()
+ defer ui.Window.Invalidate()
+ defer ui.State.Unlock()
+
+ ui.State.UrlEditor.ReadOnly = isPlaying
+ if isPlaying {
+ ui.State.PlayPauseButtonText = "Stop"
+ } else {
+ ui.State.PlayPauseButtonText = "Play"
+ }
+ }
+
+ setPlayingState(true)
+
+ cmd := exec.CommandContext(
+ ctx,
+ "mpv",
+ "--no-video",
+ url,
+ )
+ _ = cmd.Run()
+
+ setPlayingState(false)
+}