Improving backend

This commit is contained in:
nemunaire 2023-11-13 13:19:07 +01:00
parent e2013351d1
commit c67b43ab3c
9 changed files with 301 additions and 47 deletions

View File

@ -21,9 +21,12 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
ret := map[string]*SourceState{} ret := map[string]*SourceState{}
for k, src := range sources.SoundSources { for k, src := range sources.SoundSources {
active := src.IsActive()
ret[k] = &SourceState{ ret[k] = &SourceState{
Name: src.GetName(), Name: src.GetName(),
Enabled: src.IsEnabled(), Enabled: src.IsEnabled(),
Active: &active,
} }
} }
@ -47,6 +50,22 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
sourcesRoutes.GET("/settings", func(c *gin.Context) { sourcesRoutes.GET("/settings", func(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("source")) c.JSON(http.StatusOK, c.MustGet("source"))
}) })
sourcesRoutes.GET("/currently", func(c *gin.Context) {
src := c.MustGet("source").(sources.SoundSource)
if !src.IsActive() {
c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{"errmsg": "Source not active"})
return
}
s, ok := src.(sources.PlayingSource)
if !ok {
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support"})
return
}
c.JSON(http.StatusOK, s.CurrentlyPlaying())
})
sourcesRoutes.POST("/enable", func(c *gin.Context) { sourcesRoutes.POST("/enable", func(c *gin.Context) {
src := c.MustGet("source").(sources.SoundSource) src := c.MustGet("source").(sources.SoundSource)

View File

@ -2,7 +2,9 @@ package api
import ( import (
"bufio" "bufio"
"flag"
"fmt" "fmt"
"log"
"net/http" "net/http"
"os/exec" "os/exec"
"strconv" "strconv"
@ -13,6 +15,12 @@ import (
"git.nemunai.re/nemunaire/hathoris/config" "git.nemunai.re/nemunaire/hathoris/config"
) )
var cardId string = "0"
func init() {
flag.StringVar(&cardId, "card-id", cardId, "ALSA card identifier for volume handling")
}
func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) {
router.GET("/mixer", func(c *gin.Context) { router.GET("/mixer", func(c *gin.Context) {
cnt, err := parseAmixerContent() cnt, err := parseAmixerContent()
@ -21,7 +29,13 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) {
return return
} }
c.JSON(http.StatusOK, cnt) var ret []*CardControlState
for _, cc := range cnt {
ret = append(ret, cc.ToCardControlState())
}
c.JSON(http.StatusOK, ret)
}) })
mixerRoutes := router.Group("/mixer/:mixer") mixerRoutes := router.Group("/mixer/:mixer")
@ -45,10 +59,12 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) {
var values []string var values []string
for _, v := range valuesINT { for _, v := range valuesINT {
if t, ok := v.(int64); ok { if t, ok := v.(float64); ok {
values = append(values, strconv.FormatInt(t, 10)) if float64(int64(t)) == t {
} else if t, ok := v.(float64); ok { values = append(values, strconv.FormatInt(int64(t), 10))
} else {
values = append(values, fmt.Sprintf("%f", t)) values = append(values, fmt.Sprintf("%f", t))
}
} else if t, ok := v.(bool); ok { } else if t, ok := v.(bool); ok {
if t { if t {
values = append(values, "on") values = append(values, "on")
@ -110,7 +126,7 @@ func (cc *CardControl) parseAmixerField(key, value string) (err error) {
case "iface": case "iface":
cc.Interface = value cc.Interface = value
case "name": case "name":
cc.Name = value cc.Name = strings.TrimPrefix(strings.TrimSuffix(value, "'"), "'")
case "type": case "type":
cc.Type = value cc.Type = value
case "access": case "access":
@ -133,12 +149,17 @@ func (cc *CardControl) ToCardControlState() *CardControlState {
NumID: cc.NumID, NumID: cc.NumID,
Type: cc.Type, Type: cc.Type,
Name: cc.Name, Name: cc.Name,
RW: strings.HasPrefix(cc.Access, "rw"),
Items: cc.Items, Items: cc.Items,
} }
if cc.DBScale.Min != 0 || cc.DBScale.Step != 0 {
ccs.DBScale = &cc.DBScale
}
// Convert values // Convert values
for _, v := range cc.Values { for _, v := range cc.Values {
if cc.Type == "INTEGER" { if cc.Type == "INTEGER" || cc.Type == "ENUMERATED" {
if tmp, err := strconv.ParseFloat(v, 10); err == nil { if tmp, err := strconv.ParseFloat(v, 10); err == nil {
ccs.Current = append(ccs.Current, tmp) ccs.Current = append(ccs.Current, tmp)
} }
@ -151,25 +172,8 @@ func (cc *CardControl) ToCardControlState() *CardControlState {
} }
} }
if cc.DBScale.Min != 0 { ccs.Min = cc.Min
ccs.Min = cc.DBScale.Min ccs.Max = cc.Max
ccs.Unit = "dB"
} else if cc.Min != 0 {
ccs.Min = float64(cc.Min)
}
if cc.DBScale.Step != 0 {
ccs.Step = cc.DBScale.Step
ccs.Unit = "dB"
} else if cc.Step != 0 {
ccs.Step = float64(cc.Step)
} else {
ccs.Step = 1.0
}
if cc.Max != 0 {
ccs.Max = ccs.Min + ccs.Step*float64(cc.Max-cc.Min)
}
return ccs return ccs
} }
@ -177,7 +181,7 @@ func (cc *CardControl) ToCardControlState() *CardControlState {
type CardControldBScale struct { type CardControldBScale struct {
Min float64 Min float64
Step float64 Step float64
Mute int64 Mute int64 `json:",omitempty"`
} }
func (cc *CardControldBScale) parseAmixerField(key, value string) (err error) { func (cc *CardControldBScale) parseAmixerField(key, value string) (err error) {
@ -197,16 +201,16 @@ type CardControlState struct {
NumID int64 NumID int64
Name string Name string
Type string Type string
Min float64 RW bool `json:"RW,omitempty"`
Max float64 Min int64
Step float64 Max int64
Unit string `json:"unit,omitempty"` DBScale *CardControldBScale `json:",omitempty"`
Current []interface{} `json:"values,omitempty"` Current []interface{} `json:"values,omitempty"`
Items []string `json:"items,omitempty"` Items []string `json:"items,omitempty"`
} }
func parseAmixerContent() ([]*CardControl, error) { func parseAmixerContent() ([]*CardControl, error) {
cmd := exec.Command("amixer", "-c1", "contents") cmd := exec.Command("amixer", "-c", cardId, "-M", "contents")
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
@ -274,11 +278,14 @@ func parseAmixerContent() ([]*CardControl, error) {
func (cc *CardControl) CsetAmixer(values ...string) error { func (cc *CardControl) CsetAmixer(values ...string) error {
opts := []string{ opts := []string{
"-c1", "-c",
cardId,
"-M",
"cset", "cset",
fmt.Sprintf("numid=%d", cc.NumID), fmt.Sprintf("numid=%d", cc.NumID),
} }
opts = append(opts, values...) opts = append(opts, strings.Join(values, ","))
log.Println(opts)
cmd := exec.Command("amixer", opts...) cmd := exec.Command("amixer", opts...)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {

6
go.mod
View File

@ -2,7 +2,10 @@ module git.nemunai.re/nemunaire/hathoris
go 1.21 go 1.21
require github.com/gin-gonic/gin v1.9.1 require (
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37
github.com/gin-gonic/gin v1.9.1
)
require ( require (
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect
@ -28,5 +31,6 @@ require (
golang.org/x/sys v0.8.0 // indirect golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

4
go.sum
View File

@ -1,3 +1,5 @@
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37 h1:/oQBAuySCcme0DLhicWkr7FaAT5nh1XbbbnCMR2WdPA=
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37/go.mod h1:nMVB54ifXmC1hpgfq7gTpotbv891pd2wAX/whuUj1q4=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@ -80,6 +82,8 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -9,6 +9,7 @@ import (
"git.nemunai.re/nemunaire/hathoris/config" "git.nemunai.re/nemunaire/hathoris/config"
_ "git.nemunai.re/nemunaire/hathoris/sources/amp1_gpio" _ "git.nemunai.re/nemunaire/hathoris/sources/amp1_gpio"
_ "git.nemunai.re/nemunaire/hathoris/sources/mpv" _ "git.nemunai.re/nemunaire/hathoris/sources/mpv"
_ "git.nemunai.re/nemunaire/hathoris/sources/spdif"
) )
var ( var (

View File

@ -2,15 +2,18 @@ package amp1gpio
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"log" "log"
"os" "os"
"os/exec"
"path" "path"
"git.nemunai.re/nemunaire/hathoris/sources" "git.nemunai.re/nemunaire/hathoris/sources"
) )
type AMP1GPIOSource struct { type AMP1GPIOSource struct {
process *exec.Cmd
Path string Path string
} }
@ -39,7 +42,7 @@ func (s *AMP1GPIOSource) read() ([]byte, error) {
} }
func (s *AMP1GPIOSource) IsActive() bool { func (s *AMP1GPIOSource) IsActive() bool {
return s.IsEnabled() return s.process != nil
} }
func (s *AMP1GPIOSource) IsEnabled() bool { func (s *AMP1GPIOSource) IsEnabled() bool {
@ -49,7 +52,7 @@ func (s *AMP1GPIOSource) IsEnabled() bool {
return false return false
} }
return bytes.Compare(b, []byte{'1'}) == 0 return bytes.Compare(b, []byte{'1', '\n'}) == 0
} }
func (s *AMP1GPIOSource) write(value string) error { func (s *AMP1GPIOSource) write(value string) error {
@ -65,9 +68,33 @@ func (s *AMP1GPIOSource) write(value string) error {
} }
func (s *AMP1GPIOSource) Enable() error { func (s *AMP1GPIOSource) Enable() error {
if s.process != nil {
return fmt.Errorf("Already running")
}
s.process = exec.Command("aplay", "-f", "cd", "/dev/zero")
if err := s.process.Start(); err != nil {
return err
}
go func() {
err := s.process.Wait()
if err != nil {
s.process.Process.Kill()
}
s.process = nil
}()
return s.write("1") return s.write("1")
} }
func (s *AMP1GPIOSource) Disable() error { func (s *AMP1GPIOSource) Disable() error {
if s.process != nil {
if s.process.Process != nil {
s.process.Process.Kill()
}
}
return s.write("0") return s.write("0")
} }

View File

@ -11,3 +11,7 @@ type SoundSource interface {
Enable() error Enable() error
Disable() error Disable() error
} }
type PlayingSource interface {
CurrentlyPlaying() string
}

View File

@ -2,26 +2,41 @@ package mpv
import ( import (
"fmt" "fmt"
"log"
"os"
"os/exec" "os/exec"
"time"
"github.com/DexterLB/mpvipc"
"git.nemunai.re/nemunaire/hathoris/sources" "git.nemunai.re/nemunaire/hathoris/sources"
) )
type MPVSource struct { type MPVSource struct {
process *exec.Cmd process *exec.Cmd
ipcSocket string
Name string
Options []string Options []string
File string File string
} }
func init() { func init() {
sources.SoundSources["mpv"] = &MPVSource{ sources.SoundSources["mpv-1"] = &MPVSource{
Options: []string{"--no-video"}, Name: "Radio 1",
ipcSocket: "/tmp/tmpmpv.radio-1",
Options: []string{"--no-video", "--no-terminal"},
File: "https://mediaserv38.live-streams.nl:18030/stream", File: "https://mediaserv38.live-streams.nl:18030/stream",
} }
sources.SoundSources["mpv-2"] = &MPVSource{
Name: "Radio 2",
ipcSocket: "/tmp/tmpmpv.radio-2",
Options: []string{"--no-video", "--no-terminal"},
File: "https://mediaserv38.live-streams.nl:18040/live",
}
} }
func (s *MPVSource) GetName() string { func (s *MPVSource) GetName() string {
return "Radio 1" return s.Name
} }
func (s *MPVSource) IsActive() bool { func (s *MPVSource) IsActive() bool {
@ -39,6 +54,9 @@ func (s *MPVSource) Enable() (err error) {
var opts []string var opts []string
opts = append(opts, s.Options...) opts = append(opts, s.Options...)
if s.ipcSocket != "" {
opts = append(opts, "--input-ipc-server="+s.ipcSocket, "--pause")
}
opts = append(opts, s.File) opts = append(opts, s.File)
s.process = exec.Command("mpv", opts...) s.process = exec.Command("mpv", opts...)
@ -55,6 +73,39 @@ func (s *MPVSource) Enable() (err error) {
s.process = nil s.process = nil
}() }()
if s.ipcSocket != "" {
_, err = os.Stat(s.ipcSocket)
for i := 20; i >= 0 && err != nil; i-- {
time.Sleep(100 * time.Millisecond)
_, err = os.Stat(s.ipcSocket)
}
time.Sleep(200 * time.Millisecond)
conn := mpvipc.NewConnection(s.ipcSocket)
err = conn.Open()
for i := 20; i >= 0 && err != nil; i-- {
time.Sleep(100 * time.Millisecond)
err = conn.Open()
}
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Get("media-title")
for err != nil {
time.Sleep(100 * time.Millisecond)
_, err = conn.Get("media-title")
}
conn.Set("ao-volume", 50)
err = conn.Set("pause", false)
if err != nil {
return err
}
}
return return
} }
@ -67,3 +118,24 @@ func (s *MPVSource) Disable() error {
return nil return nil
} }
func (s *MPVSource) CurrentlyPlaying() string {
if s.ipcSocket != "" {
conn := mpvipc.NewConnection(s.ipcSocket)
err := conn.Open()
if err != nil {
log.Println("Unable to open mpv socket:", err.Error())
return "!"
}
defer conn.Close()
title, err := conn.Get("media-title")
if err != nil {
log.Println("Unable to retrieve title:", err.Error())
return "!"
}
return title.(string)
}
return "-"
}

116
sources/spdif/source.go Normal file
View File

@ -0,0 +1,116 @@
package spdif
import (
"fmt"
"io"
"os"
"os/exec"
"path"
"git.nemunai.re/nemunaire/hathoris/sources"
)
type SPDIFSource struct {
processRec *exec.Cmd
processPlay *exec.Cmd
DeviceIn string
DeviceOut string
Bitrate int64
Channels int64
Format string
}
func init() {
if dirs, err := os.ReadDir("/sys/class/sound"); err == nil {
for _, dir := range dirs {
thisdir := path.Join("/sys/class/sound", dir.Name())
if s, err := os.Stat(thisdir); err == nil && s.IsDir() {
idfile := path.Join(thisdir, "id")
if fd, err := os.Open(idfile); err == nil {
if cnt, err := io.ReadAll(fd); err == nil && string(cnt) == "imxspdif\n" {
sources.SoundSources["imxspdif"] = &SPDIFSource{
DeviceIn: "imxspdif",
DeviceOut: "is31ap2121",
Bitrate: 48000,
Channels: 2,
Format: "S24_LE",
}
}
fd.Close()
}
}
}
}
}
func (s *SPDIFSource) GetName() string {
return "S/PDIF"
}
func (s *SPDIFSource) IsActive() bool {
return s.processRec != nil
}
func (s *SPDIFSource) IsEnabled() bool {
return s.processRec != nil
}
func (s *SPDIFSource) Enable() error {
if s.processRec != nil {
return fmt.Errorf("Already running")
}
if s.processPlay != nil {
s.processPlay.Process.Kill()
}
pipeR, pipeW, err := os.Pipe()
if err != nil {
return err
}
s.processPlay = exec.Command("aplay", "-c", fmt.Sprintf("%d", s.Channels), "-D", "hw:"+s.DeviceOut, "--period-size=512", "-B0", "--buffer-size=512")
s.processPlay.Stdin = pipeR
if err := s.processPlay.Start(); err != nil {
return err
}
go func() {
err := s.processPlay.Wait()
if err != nil {
s.processPlay.Process.Kill()
pipeR.Close()
pipeW.Close()
}
s.processPlay = nil
}()
s.processRec = exec.Command("arecord", "-t", "wav", "-f", s.Format, fmt.Sprintf("-r%d", s.Bitrate), fmt.Sprintf("-c%d", s.Channels), "-D", "hw:"+s.DeviceIn, "-B0", "--buffer-size=512")
s.processRec.Stdout = pipeW
if err := s.processRec.Start(); err != nil {
s.processPlay.Process.Kill()
return err
}
go func() {
err := s.processRec.Wait()
if err != nil {
s.processRec.Process.Kill()
}
s.processRec = nil
}()
return nil
}
func (s *SPDIFSource) Disable() error {
if s.processRec != nil && s.processRec.Process != nil {
s.processRec.Process.Kill()
}
if s.processPlay != nil && s.processPlay.Process != nil {
s.processPlay.Process.Kill()
}
return nil
}