diff --git a/alsacontrol/cardcontrol.go b/alsacontrol/cardcontrol.go deleted file mode 100644 index a8c3804..0000000 --- a/alsacontrol/cardcontrol.go +++ /dev/null @@ -1,204 +0,0 @@ -package alsa - -import ( - "bufio" - "fmt" - "os/exec" - "strconv" - "strings" -) - -type CardControl struct { - NumID int64 - Interface string - Name string - Type string - Access string - NValues int64 - Min int64 - Max int64 - Step int64 - DBScale CardControldBScale - Values []string - Items []string -} - -func (cc *CardControl) parseAmixerField(key, value string) (err error) { - switch key { - case "numid": - cc.NumID, err = strconv.ParseInt(value, 10, 64) - case "iface": - cc.Interface = value - case "name": - cc.Name = strings.TrimPrefix(strings.TrimSuffix(value, "'"), "'") - case "type": - cc.Type = value - case "access": - cc.Access = value - case "values": - cc.NValues, err = strconv.ParseInt(value, 10, 64) - case "min": - cc.Min, err = strconv.ParseInt(value, 10, 64) - case "max": - cc.Max, err = strconv.ParseInt(value, 10, 64) - case "step": - cc.Step, err = strconv.ParseInt(value, 10, 64) - } - - return -} - -func (cc *CardControl) ToCardControlState() *CardControlState { - ccs := &CardControlState{ - NumID: cc.NumID, - Type: cc.Type, - Name: cc.Name, - RW: strings.HasPrefix(cc.Access, "rw"), - Items: cc.Items, - } - - if cc.DBScale.Min != 0 || cc.DBScale.Step != 0 { - ccs.DBScale = &cc.DBScale - } - - // Convert values - for _, v := range cc.Values { - if cc.Type == "INTEGER" || cc.Type == "ENUMERATED" { - if tmp, err := strconv.ParseFloat(v, 10); err == nil { - ccs.Current = append(ccs.Current, tmp) - } - } else if cc.Type == "BOOLEAN" { - if v == "on" { - ccs.Current = append(ccs.Current, true) - } else { - ccs.Current = append(ccs.Current, false) - } - } - } - - ccs.Min = cc.Min - ccs.Max = cc.Max - - return ccs -} - -type CardControldBScale struct { - Min float64 - Step float64 - Mute int64 `json:",omitempty"` -} - -func (cc *CardControldBScale) parseAmixerField(key, value string) (err error) { - switch key { - case "min": - cc.Min, err = strconv.ParseFloat(strings.TrimSuffix(value, "dB"), 10) - case "step": - cc.Step, err = strconv.ParseFloat(strings.TrimSuffix(value, "dB"), 10) - case "mute": - cc.Mute, err = strconv.ParseInt(value, 10, 64) - } - - return -} - -type CardControlState struct { - NumID int64 - Name string - Type string - RW bool `json:"RW,omitempty"` - Min int64 - Max int64 - DBScale *CardControldBScale `json:",omitempty"` - Current []interface{} `json:"values,omitempty"` - Items []string `json:"items,omitempty"` -} - -func ParseAmixerContent(cardId string) ([]*CardControl, error) { - cardIdType := "-D" - if _, err := strconv.Atoi(cardId); err == nil { - cardIdType = "-c" - } - - cmd := exec.Command("amixer", cardIdType, cardId, "-M", "contents") - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, err - } - if err := cmd.Start(); err != nil { - return nil, err - } - - var ret []*CardControl - - fscanner := bufio.NewScanner(stdout) - for fscanner.Scan() { - line := fscanner.Text() - - if strings.HasPrefix(line, " ; Item #") { - cc := ret[len(ret)-1] - cc.Items = append(cc.Items, strings.TrimSuffix(line[strings.Index(line, "'")+1:], "'")) - } else if strings.HasPrefix(line, " :") { - cc := ret[len(ret)-1] - line = strings.TrimPrefix(line, " : ") - - kv := strings.SplitN(line, "=", 2) - if kv[0] == "values" { - cc.Values = strings.Split(kv[1], ",") - } - } else if strings.HasPrefix(line, " |") || strings.HasPrefix(line, " |") { - cc := ret[len(ret)-1] - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "| dBscale-") { - line = strings.TrimPrefix(line, "| dBscale-") - - fields := strings.Split(line, ",") - - var scale CardControldBScale - for _, field := range fields { - kv := strings.SplitN(field, "=", 2) - scale.parseAmixerField(kv[0], kv[1]) - } - cc.DBScale = scale - } - } else { - var cc *CardControl - - if strings.HasPrefix(line, "numid=") { - cc = &CardControl{} - ret = append(ret, cc) - } else { - cc = ret[len(ret)-1] - line = strings.TrimPrefix(line, " ; ") - } - - fields := strings.Split(line, ",") - - for _, field := range fields { - kv := strings.SplitN(field, "=", 2) - cc.parseAmixerField(kv[0], kv[1]) - } - } - } - - err = cmd.Wait() - return ret, err -} - -func (cc *CardControl) CsetAmixer(cardId string, values ...string) error { - opts := []string{ - "-c", - cardId, - "-M", - "cset", - fmt.Sprintf("numid=%d", cc.NumID), - } - opts = append(opts, strings.Join(values, ",")) - cmd := exec.Command("amixer", opts...) - - if err := cmd.Start(); err != nil { - return err - } - - return cmd.Wait() -} diff --git a/api/volume.go b/api/volume.go index bbb0141..f633c7e 100644 --- a/api/volume.go +++ b/api/volume.go @@ -1,14 +1,16 @@ package api import ( + "bufio" "flag" "fmt" "net/http" + "os/exec" "strconv" + "strings" "github.com/gin-gonic/gin" - "git.nemunai.re/nemunaire/hathoris/alsacontrol" "git.nemunai.re/nemunaire/hathoris/config" ) @@ -20,13 +22,13 @@ func init() { func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { router.GET("/mixer", func(c *gin.Context) { - cnt, err := alsa.ParseAmixerContent(cardId) + cnt, err := parseAmixerContent() if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } - var ret []*alsa.CardControlState + var ret []*CardControlState for _, cc := range cnt { ret = append(ret, cc.ToCardControlState()) @@ -39,12 +41,12 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { mixerRoutes.Use(MixerHandler) mixerRoutes.GET("", func(c *gin.Context) { - cc := c.MustGet("mixer").(*alsa.CardControl) + cc := c.MustGet("mixer").(*CardControl) c.JSON(http.StatusOK, cc.ToCardControlState()) }) mixerRoutes.POST("/values", func(c *gin.Context) { - cc := c.MustGet("mixer").(*alsa.CardControl) + cc := c.MustGet("mixer").(*CardControl) var valuesINT []interface{} @@ -71,7 +73,7 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { } } - err = cc.CsetAmixer(cardId, values...) + err = cc.CsetAmixer(values...) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to set values: %s", err.Error())}) return @@ -82,7 +84,7 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { } func MixerHandler(c *gin.Context) { - mixers, err := alsa.ParseAmixerContent(cardId) + mixers, err := parseAmixerContent() if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return @@ -100,3 +102,193 @@ func MixerHandler(c *gin.Context) { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Mixer not found"}) } + +type CardControl struct { + NumID int64 + Interface string + Name string + Type string + Access string + NValues int64 + Min int64 + Max int64 + Step int64 + DBScale CardControldBScale + Values []string + Items []string +} + +func (cc *CardControl) parseAmixerField(key, value string) (err error) { + switch key { + case "numid": + cc.NumID, err = strconv.ParseInt(value, 10, 64) + case "iface": + cc.Interface = value + case "name": + cc.Name = strings.TrimPrefix(strings.TrimSuffix(value, "'"), "'") + case "type": + cc.Type = value + case "access": + cc.Access = value + case "values": + cc.NValues, err = strconv.ParseInt(value, 10, 64) + case "min": + cc.Min, err = strconv.ParseInt(value, 10, 64) + case "max": + cc.Max, err = strconv.ParseInt(value, 10, 64) + case "step": + cc.Step, err = strconv.ParseInt(value, 10, 64) + } + + return +} + +func (cc *CardControl) ToCardControlState() *CardControlState { + ccs := &CardControlState{ + NumID: cc.NumID, + Type: cc.Type, + Name: cc.Name, + RW: strings.HasPrefix(cc.Access, "rw"), + Items: cc.Items, + } + + if cc.DBScale.Min != 0 || cc.DBScale.Step != 0 { + ccs.DBScale = &cc.DBScale + } + + // Convert values + for _, v := range cc.Values { + if cc.Type == "INTEGER" || cc.Type == "ENUMERATED" { + if tmp, err := strconv.ParseFloat(v, 10); err == nil { + ccs.Current = append(ccs.Current, tmp) + } + } else if cc.Type == "BOOLEAN" { + if v == "on" { + ccs.Current = append(ccs.Current, true) + } else { + ccs.Current = append(ccs.Current, false) + } + } + } + + ccs.Min = cc.Min + ccs.Max = cc.Max + + return ccs +} + +type CardControldBScale struct { + Min float64 + Step float64 + Mute int64 `json:",omitempty"` +} + +func (cc *CardControldBScale) parseAmixerField(key, value string) (err error) { + switch key { + case "min": + cc.Min, err = strconv.ParseFloat(strings.TrimSuffix(value, "dB"), 10) + case "step": + cc.Step, err = strconv.ParseFloat(strings.TrimSuffix(value, "dB"), 10) + case "mute": + cc.Mute, err = strconv.ParseInt(value, 10, 64) + } + + return +} + +type CardControlState struct { + NumID int64 + Name string + Type string + RW bool `json:"RW,omitempty"` + Min int64 + Max int64 + DBScale *CardControldBScale `json:",omitempty"` + Current []interface{} `json:"values,omitempty"` + Items []string `json:"items,omitempty"` +} + +func parseAmixerContent() ([]*CardControl, error) { + cmd := exec.Command("amixer", "-c", cardId, "-M", "contents") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + + var ret []*CardControl + + fscanner := bufio.NewScanner(stdout) + for fscanner.Scan() { + line := fscanner.Text() + + if strings.HasPrefix(line, " ; Item #") { + cc := ret[len(ret)-1] + cc.Items = append(cc.Items, strings.TrimSuffix(line[strings.Index(line, "'")+1:], "'")) + } else if strings.HasPrefix(line, " :") { + cc := ret[len(ret)-1] + line = strings.TrimPrefix(line, " : ") + + kv := strings.SplitN(line, "=", 2) + if kv[0] == "values" { + cc.Values = strings.Split(kv[1], ",") + } + } else if strings.HasPrefix(line, " |") || strings.HasPrefix(line, " |") { + cc := ret[len(ret)-1] + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "| dBscale-") { + line = strings.TrimPrefix(line, "| dBscale-") + + fields := strings.Split(line, ",") + + var scale CardControldBScale + for _, field := range fields { + kv := strings.SplitN(field, "=", 2) + scale.parseAmixerField(kv[0], kv[1]) + } + cc.DBScale = scale + } + } else { + var cc *CardControl + + if strings.HasPrefix(line, "numid=") { + cc = &CardControl{} + ret = append(ret, cc) + } else { + cc = ret[len(ret)-1] + line = strings.TrimPrefix(line, " ; ") + } + + fields := strings.Split(line, ",") + + for _, field := range fields { + kv := strings.SplitN(field, "=", 2) + cc.parseAmixerField(kv[0], kv[1]) + } + } + } + + err = cmd.Wait() + return ret, err +} + +func (cc *CardControl) CsetAmixer(values ...string) error { + opts := []string{ + "-c", + cardId, + "-M", + "cset", + fmt.Sprintf("numid=%d", cc.NumID), + } + opts = append(opts, strings.Join(values, ",")) + cmd := exec.Command("amixer", opts...) + + if err := cmd.Start(); err != nil { + return err + } + + return cmd.Wait() +} diff --git a/sources/spdif/source.go b/sources/spdif/source.go index d37af85..2e086ff 100644 --- a/sources/spdif/source.go +++ b/sources/spdif/source.go @@ -3,21 +3,16 @@ package spdif import ( "fmt" "io" - "log" "os" "os/exec" "path" - "strconv" - "time" - "git.nemunai.re/nemunaire/hathoris/alsacontrol" "git.nemunai.re/nemunaire/hathoris/sources" ) type SPDIFSource struct { processRec *exec.Cmd processPlay *exec.Cmd - endChan chan bool DeviceIn string DeviceOut string Bitrate int64 @@ -33,16 +28,12 @@ func init() { 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" { - sr, err := getCardSampleRate("imxspdif") - if err == nil { - sources.SoundSources["imxspdif"] = &SPDIFSource{ - endChan: make(chan bool, 1), - DeviceIn: "imxspdif", - DeviceOut: "is31ap2121", - Bitrate: sr, - Channels: 2, - Format: "S24_LE", - } + sources.SoundSources["imxspdif"] = &SPDIFSource{ + DeviceIn: "imxspdif", + DeviceOut: "is31ap2121", + Bitrate: 48000, + Channels: 2, + Format: "S24_LE", } } fd.Close() @@ -95,15 +86,13 @@ func (s *SPDIFSource) Enable() error { 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, "-F0", "--period-size=512", "-B0", "--buffer-size=512") + 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 s.watchBitrate() - go func() { err := s.processRec.Wait() if err != nil { @@ -124,57 +113,6 @@ func (s *SPDIFSource) Disable() error { if s.processPlay != nil && s.processPlay.Process != nil { s.processPlay.Process.Kill() } - s.endChan <- true return nil } - -func getCardSampleRate(cardId string) (sr int64, err error) { - cc, err := alsa.ParseAmixerContent("hw:" + cardId) - if err != nil { - return 0, fmt.Errorf("unable to parse amixer content: %w", err) - } - - for _, c := range cc { - if len(c.Values) == 1 { - val, err := strconv.Atoi(c.Values[0]) - if c.Name == "RX Sample Rate" && err == nil && val > 0 { - return int64(val), nil - } - } - } - - return 0, fmt.Errorf("unable to find 'RX Sample Rate' control value") -} - -func (s *SPDIFSource) watchBitrate() { - ticker := time.NewTicker(time.Second) - -loop: - for { - select { - case <-ticker.C: - sr, err := getCardSampleRate(s.DeviceIn) - if err == nil && s.Bitrate/10 != sr/10 { - log.Printf("[SPDIF] Sample rate changes from %d to %d Hz", s.Bitrate, sr) - s.Bitrate = sr - - s.Disable() - - // Wait process exited - for { - if s.processPlay == nil && s.processRec == nil { - break - } - time.Sleep(100 * time.Millisecond) - } - - s.Enable() - } - case <-s.endChan: - break loop - } - } - - ticker.Stop() -}