From bcb6b61af59c880b94c90bf07ba08a10dbafc458 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 21 Nov 2024 16:21:59 +0100 Subject: [PATCH 1/4] imxspdif: Fix crackling sound --- sources/spdif/source.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/spdif/source.go b/sources/spdif/source.go index 2e086ff..6d0f3c3 100644 --- a/sources/spdif/source.go +++ b/sources/spdif/source.go @@ -86,7 +86,7 @@ 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, "-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, "-F0", "--period-size=512", "-B0", "--buffer-size=512") s.processRec.Stdout = pipeW if err := s.processRec.Start(); err != nil { s.processPlay.Process.Kill() From d9a05519372f8b331f8d9828ebd11846fa360c63 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 21 Nov 2024 16:40:20 +0100 Subject: [PATCH 2/4] Refactor amixer parser --- alsacontrol/cardcontrol.go | 199 +++++++++++++++++++++++++++++++++++ api/volume.go | 206 ++----------------------------------- 2 files changed, 206 insertions(+), 199 deletions(-) create mode 100644 alsacontrol/cardcontrol.go diff --git a/alsacontrol/cardcontrol.go b/alsacontrol/cardcontrol.go new file mode 100644 index 0000000..429a317 --- /dev/null +++ b/alsacontrol/cardcontrol.go @@ -0,0 +1,199 @@ +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) { + 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(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 f633c7e..bbb0141 100644 --- a/api/volume.go +++ b/api/volume.go @@ -1,16 +1,14 @@ 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" ) @@ -22,13 +20,13 @@ func init() { func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { router.GET("/mixer", func(c *gin.Context) { - cnt, err := parseAmixerContent() + cnt, err := alsa.ParseAmixerContent(cardId) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } - var ret []*CardControlState + var ret []*alsa.CardControlState for _, cc := range cnt { ret = append(ret, cc.ToCardControlState()) @@ -41,12 +39,12 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { mixerRoutes.Use(MixerHandler) mixerRoutes.GET("", func(c *gin.Context) { - cc := c.MustGet("mixer").(*CardControl) + cc := c.MustGet("mixer").(*alsa.CardControl) c.JSON(http.StatusOK, cc.ToCardControlState()) }) mixerRoutes.POST("/values", func(c *gin.Context) { - cc := c.MustGet("mixer").(*CardControl) + cc := c.MustGet("mixer").(*alsa.CardControl) var valuesINT []interface{} @@ -73,7 +71,7 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { } } - err = cc.CsetAmixer(values...) + err = cc.CsetAmixer(cardId, values...) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to set values: %s", err.Error())}) return @@ -84,7 +82,7 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) { } func MixerHandler(c *gin.Context) { - mixers, err := parseAmixerContent() + mixers, err := alsa.ParseAmixerContent(cardId) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return @@ -102,193 +100,3 @@ 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() -} From bb2f432a4bcb514a5f5b982c5e6b3e9b49ee25c8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 21 Nov 2024 17:51:16 +0100 Subject: [PATCH 3/4] alsacontrol: Allow to pass hw: instead of cardid --- alsacontrol/cardcontrol.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/alsacontrol/cardcontrol.go b/alsacontrol/cardcontrol.go index 429a317..a8c3804 100644 --- a/alsacontrol/cardcontrol.go +++ b/alsacontrol/cardcontrol.go @@ -114,7 +114,12 @@ type CardControlState struct { } func ParseAmixerContent(cardId string) ([]*CardControl, error) { - cmd := exec.Command("amixer", "-c", cardId, "-M", "contents") + 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 { From 84d594d69527995defa57c600fcc88a0d2043bed Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 21 Nov 2024 17:51:45 +0100 Subject: [PATCH 4/4] spdif: Handle interface sample rate changes --- sources/spdif/source.go | 74 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/sources/spdif/source.go b/sources/spdif/source.go index 6d0f3c3..d37af85 100644 --- a/sources/spdif/source.go +++ b/sources/spdif/source.go @@ -3,16 +3,21 @@ 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 @@ -28,12 +33,16 @@ 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" { - sources.SoundSources["imxspdif"] = &SPDIFSource{ - DeviceIn: "imxspdif", - DeviceOut: "is31ap2121", - Bitrate: 48000, - Channels: 2, - Format: "S24_LE", + 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", + } } } fd.Close() @@ -93,6 +102,8 @@ func (s *SPDIFSource) Enable() error { return err } + go s.watchBitrate() + go func() { err := s.processRec.Wait() if err != nil { @@ -113,6 +124,57 @@ 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() +}