Skip to content

Commit

Permalink
feat(linux): ✨ migrate to different pulseaudio library
Browse files Browse the repository at this point in the history
- migrate to github.com/jfreymuth/pulse/proto for pulseaudio
- fix mute switch not being associated with MQTT device
  • Loading branch information
joshuar committed May 23, 2024
1 parent 51d5f54 commit e5c576c
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 65 deletions.
8 changes: 0 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,13 @@ require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/jaypipes/pcidb v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mandykoh/go-parallel v0.1.0 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/sync v0.7.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.0 // indirect
)
Expand All @@ -49,7 +44,6 @@ require (
github.com/alecthomas/kong v0.9.0
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fearful-symmetry/garlic v0.3.0
github.com/fredbi/uri v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
Expand All @@ -76,7 +70,6 @@ require (
github.com/mandykoh/prism v0.35.2
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2
github.com/miekg/dns v1.1.27 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
Expand All @@ -95,5 +88,4 @@ require (
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
mrogalski.eu/go/pulseaudio v0.0.0-20240327130323-384e01075e6e
)
18 changes: 0 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fearful-symmetry/garlic v0.3.0 h1:ZAL/e5BhH6lKTt5FqwoxyFk0rB9E+Ld2omjQFnvfAyU=
github.com/fearful-symmetry/garlic v0.3.0/go.mod h1:+hcj5tRGZdwY6RKMopfdnlMAMepQz4Nbaw7A12MCNWk=
github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg=
github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
Expand Down Expand Up @@ -254,10 +252,6 @@ github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:Yiu
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jfreymuth/pulse v0.1.1 h1:9WLNBNCijmtZ14ZJpatgJPu/NjwAl3TIKItSFnTh+9A=
github.com/jfreymuth/pulse v0.1.1/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/joshuar/go-hass-anything/v9 v9.1.0 h1:dPNfCrMVu1k6+0vnNqm8ncAynAuckrhL6RHpaULgpiY=
github.com/joshuar/go-hass-anything/v9 v9.1.0/go.mod h1:kaNCsLaXjI+7yCnBwO2ZbUViQkoiriu52i8lnPx/NAQ=
github.com/joshuar/go-hass-anything/v9 v9.2.0 h1:+VszoYfyo1Kmpqr3LRYd75Guqng0oboxjn/d1LZ6gR4=
github.com/joshuar/go-hass-anything/v9 v9.2.0/go.mod h1:0UKaTpU52i/skJQ8Ci+szRT7ztIDGoP7agvstxIVX50=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
Expand Down Expand Up @@ -299,14 +293,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v0.0.0-20180912140650-18e318c2e5d1 h1:pDJgDg70my00+GUR5Hu+tVnd939Z5eR/DGXNQqCCO74=
github.com/mdlayher/netlink v0.0.0-20180912140650-18e318c2e5d1/go.mod h1:a3TlQHkJH2m32RF224Z7LhD5N4mpyR8eUbCoYHywrwg=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
Expand Down Expand Up @@ -486,7 +472,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20170809000501-1c05540f6879/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -558,7 +543,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20170809190605-e42485b6e20a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down Expand Up @@ -837,8 +821,6 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
mrogalski.eu/go/pulseaudio v0.0.0-20240327130323-384e01075e6e h1:SQSUCiMUx1MukBgCf+pYWTOCoV0Y2YbQ0y2vqBvCY50=
mrogalski.eu/go/pulseaudio v0.0.0-20240327130323-384e01075e6e/go.mod h1:C3V0v+gsiHHbMtFJvwozjNVdPJZw9oUxlRTVc619wSU=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
70 changes: 31 additions & 39 deletions internal/linux/media/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"encoding/json"
"strconv"

"mrogalski.eu/go/pulseaudio"

"github.com/eclipse/paho.golang/paho"
mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass"
mqttapi "github.com/joshuar/go-hass-anything/v9/pkg/mqtt"
Expand All @@ -20,10 +18,11 @@ import (

"github.com/joshuar/go-hass-agent/internal/linux"
"github.com/joshuar/go-hass-agent/internal/preferences"
pulseaudiox "github.com/joshuar/go-hass-agent/pkg/linux/pulseaudio"
)

type audioDevice struct {
pulseAudio *pulseaudio.Client
pulseAudio *pulseaudiox.PulseAudioClient
msgCh chan *mqttapi.Msg
muteEntity *mqtthass.SwitchEntity
volEntity *mqtthass.NumberEntity[int]
Expand All @@ -32,7 +31,7 @@ type audioDevice struct {
func VolumeControl(ctx context.Context, msgCh chan *mqttapi.Msg) (*mqtthass.NumberEntity[int], *mqtthass.SwitchEntity) {
device := linux.MQTTDevice()

client, err := pulseaudio.NewClient()
client, err := pulseaudiox.NewPulseClient(ctx)
if err != nil {
log.Warn().Err(err).Msg("Unable to connect to Pulseaudio. Volume control will be unavailable.")
return nil, nil
Expand All @@ -56,76 +55,69 @@ func VolumeControl(ctx context.Context, msgCh chan *mqttapi.Msg) (*mqtthass.Numb

audioDev.muteEntity = mqtthass.AsSwitch(
mqtthass.NewEntity(preferences.AppName, "Mute", device.Name+"_mute").
WithOriginInfo(preferences.MQTTOrigin()).
WithDeviceInfo(device).
WithIcon("mdi:volume-mute").
WithCommandCallback(audioDev.muteCommandCallback).
WithStateCallback(audioDev.muteStateCallback).
WithValueTemplate("{{ value }}"),
true)

go func() {
log.Debug().Msg("Monitoring pulseaudio for events.")
audioDev.publishVolume()
audioDev.publishMute()
}()

go func() {
events, err := client.Updates()
if err != nil {
log.Warn().Err(err).Msg("Cannot monitor Pulseaudio.")
return
}
log.Debug().Msg("Monitoring pulseaudio for events.")
for {
select {
case <-ctx.Done():
log.Debug().Msg("Closing pulseaudio connection.")
client.Close()
return
case <-events:
audioDev.publishVolume()
audioDev.publishMute()
case <-client.EventCh:
repl, err := client.GetState()
if err != nil {
log.Debug().Err(err).Msg("Failed to parse pulseaudio state.")
continue
}
volPct := pulseaudiox.ParseVolume(repl)
switch {
case repl.Mute != client.Mute:
audioDev.publishMute()
audioDev.pulseAudio.Mute = repl.Mute
case volPct != client.Vol:
audioDev.pulseAudio.Vol = volPct
audioDev.publishVolume()
}
}
}
}()
return audioDev.volEntity, audioDev.muteEntity
}

func (d *audioDevice) getVolume() (int, error) {
v, err := d.pulseAudio.Volume()
if err != nil {
return 0, err
}
return int(v * 100), nil
}

func (d *audioDevice) setVolume(v int) error {
newVol := float32(v) / 100
return d.pulseAudio.SetVolume(newVol)
}

func (d *audioDevice) publishVolume() {
msg, err := d.volEntity.MarshalState()
if err != nil {
log.Warn().Err(err).Msg("Could not retrieve current volume.")
log.Debug().Err(err).Msg("Could not retrieve current volume.")
return
}
d.msgCh <- msg
}

func (d *audioDevice) volStateCallback(_ ...any) (json.RawMessage, error) {
vol, err := d.getVolume()
vol, err := d.pulseAudio.GetVolume()
log.Trace().Int("volume", int(vol)).Msg("Publishing volume change.")
if err != nil {
return json.RawMessage(`{ "value": 0 }`), err
}
return json.RawMessage(`{ "value": ` + strconv.Itoa(vol) + ` }`), nil
return json.RawMessage(`{ "value": ` + strconv.FormatFloat(vol, 'f', 0, 64) + ` }`), nil
}

func (d *audioDevice) volCommandCallback(p *paho.Publish) {
if newValue, err := strconv.Atoi(string(p.Payload)); err != nil {
log.Warn().Err(err).Msg("Could not parse new volume level.")
log.Debug().Err(err).Msg("Could not parse new volume level.")
} else {
log.Trace().Int("volume", newValue).Msg("Received volume change from Home Assistant.")
if err := d.setVolume(newValue); err != nil {
log.Warn().Err(err).Msg("Could not set volume level.")
if err := d.pulseAudio.SetVolume(float64(newValue)); err != nil {
log.Debug().Err(err).Msg("Could not set volume level.")
return
}
go func() {
Expand All @@ -143,21 +135,21 @@ func (d *audioDevice) setMute(v bool) {
err = d.pulseAudio.SetMute(false)
}
if err != nil {
log.Warn().Err(err).Msg("Could not set mute state.")
log.Debug().Err(err).Msg("Could not set mute state.")
}
}

func (d *audioDevice) publishMute() {
msg, err := d.muteEntity.MarshalState()
if err != nil {
log.Warn().Msg("Could not retrieve mute state.")
log.Debug().Msg("Could not retrieve mute state.")
} else {
d.msgCh <- msg
}
}

func (d *audioDevice) muteStateCallback(_ ...any) (json.RawMessage, error) {
muteState, err := d.pulseAudio.Mute()
muteState, err := d.pulseAudio.GetMute()
if err != nil {
return json.RawMessage(`OFF`), err
}
Expand Down
42 changes: 42 additions & 0 deletions pkg/linux/pulseaudio/examples/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package main

import (
"context"
"fmt"
"log/slog"

pulseaudiox "github.com/joshuar/go-hass-agent/pkg/linux/pulseaudio"
)

func main() {
client, err := pulseaudiox.NewPulseClient(context.Background())
if err != nil {
panic(err)
}

err = client.SetVolume(20)
if err != nil {
panic(err)
}
for {
<-client.EventCh
repl, err := client.GetState()
if err != nil {
slog.Error("failed to parse reply: %w", err)
}
volPct := pulseaudiox.ParseVolume(repl)
switch {
case repl.Mute != client.Mute:
fmt.Printf("mute changed to %v\n", repl.Mute)
client.Mute = repl.Mute
case volPct != client.Vol:
fmt.Printf("volume changed to %.0f%%\n", volPct)
client.Vol = volPct
}
}
}
Loading

0 comments on commit e5c576c

Please sign in to comment.