Compare commits

..

4 Commits

Author SHA1 Message Date
84d594d695 spdif: Handle interface sample rate changes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-11-21 17:51:45 +01:00
bb2f432a4b alsacontrol: Allow to pass hw: instead of cardid 2024-11-21 17:51:16 +01:00
d9a0551937 Refactor amixer parser 2024-11-21 16:40:34 +01:00
bcb6b61af5 imxspdif: Fix crackling sound 2024-11-21 16:21:59 +01:00
3 changed files with 280 additions and 206 deletions

204
alsacontrol/cardcontrol.go Normal file
View File

@ -0,0 +1,204 @@
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()
}

View File

@ -1,16 +1,14 @@
package api package api
import ( import (
"bufio"
"flag" "flag"
"fmt" "fmt"
"net/http" "net/http"
"os/exec"
"strconv" "strconv"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"git.nemunai.re/nemunaire/hathoris/alsacontrol"
"git.nemunai.re/nemunaire/hathoris/config" "git.nemunai.re/nemunaire/hathoris/config"
) )
@ -22,13 +20,13 @@ func init() {
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 := alsa.ParseAmixerContent(cardId)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
} }
var ret []*CardControlState var ret []*alsa.CardControlState
for _, cc := range cnt { for _, cc := range cnt {
ret = append(ret, cc.ToCardControlState()) ret = append(ret, cc.ToCardControlState())
@ -41,12 +39,12 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) {
mixerRoutes.Use(MixerHandler) mixerRoutes.Use(MixerHandler)
mixerRoutes.GET("", func(c *gin.Context) { mixerRoutes.GET("", func(c *gin.Context) {
cc := c.MustGet("mixer").(*CardControl) cc := c.MustGet("mixer").(*alsa.CardControl)
c.JSON(http.StatusOK, cc.ToCardControlState()) c.JSON(http.StatusOK, cc.ToCardControlState())
}) })
mixerRoutes.POST("/values", func(c *gin.Context) { mixerRoutes.POST("/values", func(c *gin.Context) {
cc := c.MustGet("mixer").(*CardControl) cc := c.MustGet("mixer").(*alsa.CardControl)
var valuesINT []interface{} 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 { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to set values: %s", err.Error())}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to set values: %s", err.Error())})
return return
@ -84,7 +82,7 @@ func declareVolumeRoutes(cfg *config.Config, router *gin.RouterGroup) {
} }
func MixerHandler(c *gin.Context) { func MixerHandler(c *gin.Context) {
mixers, err := parseAmixerContent() mixers, err := alsa.ParseAmixerContent(cardId)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
@ -102,193 +100,3 @@ func MixerHandler(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Mixer not found"}) 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()
}

View File

@ -3,16 +3,21 @@ package spdif
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"strconv"
"time"
"git.nemunai.re/nemunaire/hathoris/alsacontrol"
"git.nemunai.re/nemunaire/hathoris/sources" "git.nemunai.re/nemunaire/hathoris/sources"
) )
type SPDIFSource struct { type SPDIFSource struct {
processRec *exec.Cmd processRec *exec.Cmd
processPlay *exec.Cmd processPlay *exec.Cmd
endChan chan bool
DeviceIn string DeviceIn string
DeviceOut string DeviceOut string
Bitrate int64 Bitrate int64
@ -28,12 +33,16 @@ func init() {
idfile := path.Join(thisdir, "id") idfile := path.Join(thisdir, "id")
if fd, err := os.Open(idfile); err == nil { if fd, err := os.Open(idfile); err == nil {
if cnt, err := io.ReadAll(fd); err == nil && string(cnt) == "imxspdif\n" { if cnt, err := io.ReadAll(fd); err == nil && string(cnt) == "imxspdif\n" {
sources.SoundSources["imxspdif"] = &SPDIFSource{ sr, err := getCardSampleRate("imxspdif")
DeviceIn: "imxspdif", if err == nil {
DeviceOut: "is31ap2121", sources.SoundSources["imxspdif"] = &SPDIFSource{
Bitrate: 48000, endChan: make(chan bool, 1),
Channels: 2, DeviceIn: "imxspdif",
Format: "S24_LE", DeviceOut: "is31ap2121",
Bitrate: sr,
Channels: 2,
Format: "S24_LE",
}
} }
} }
fd.Close() fd.Close()
@ -86,13 +95,15 @@ func (s *SPDIFSource) Enable() error {
s.processPlay = nil 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 s.processRec.Stdout = pipeW
if err := s.processRec.Start(); err != nil { if err := s.processRec.Start(); err != nil {
s.processPlay.Process.Kill() s.processPlay.Process.Kill()
return err return err
} }
go s.watchBitrate()
go func() { go func() {
err := s.processRec.Wait() err := s.processRec.Wait()
if err != nil { if err != nil {
@ -113,6 +124,57 @@ func (s *SPDIFSource) Disable() error {
if s.processPlay != nil && s.processPlay.Process != nil { if s.processPlay != nil && s.processPlay.Process != nil {
s.processPlay.Process.Kill() s.processPlay.Process.Kill()
} }
s.endChan <- true
return nil 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()
}