mirror of
https://github.com/Luzifer/streamdeck.git
synced 2024-12-20 17:51:21 +00:00
Add pulsevolume action
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
554b090890
commit
f9ef030362
3 changed files with 216 additions and 21 deletions
99
cmd/streamdeck/action_pulsevolume.go
Normal file
99
cmd/streamdeck/action_pulsevolume.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerAction("pulsevolume", actionPulseVolume{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionPulseVolume struct{}
|
||||||
|
|
||||||
|
func (actionPulseVolume) Execute(attributes map[string]interface{}) error {
|
||||||
|
if pulseClient == nil {
|
||||||
|
return errors.New("PulseAudio client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
devType, ok := attributes["device"].(string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Missing 'device' attribute")
|
||||||
|
}
|
||||||
|
|
||||||
|
match, ok := attributes["match"].(string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Missing 'match' attribute")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read mute value
|
||||||
|
var (
|
||||||
|
mute string
|
||||||
|
mutev = attributes["mute"]
|
||||||
|
)
|
||||||
|
switch mutev.(type) {
|
||||||
|
case string:
|
||||||
|
mute = mutev.(string)
|
||||||
|
|
||||||
|
case bool:
|
||||||
|
mute = strconv.FormatBool(mutev.(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read volume
|
||||||
|
var (
|
||||||
|
volAbs bool
|
||||||
|
volVal float64
|
||||||
|
)
|
||||||
|
for attr, abs := range map[string]bool{
|
||||||
|
"set_volume": true,
|
||||||
|
"change_volume": false,
|
||||||
|
} {
|
||||||
|
val, ok := attributes[attr]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.(type) {
|
||||||
|
case float64:
|
||||||
|
volVal = val.(float64) / 100
|
||||||
|
|
||||||
|
case int:
|
||||||
|
volVal = float64(val.(int)) / 100
|
||||||
|
|
||||||
|
case int64:
|
||||||
|
volVal = float64(val.(int64)) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
volAbs = abs
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute change
|
||||||
|
switch devType {
|
||||||
|
|
||||||
|
case "input":
|
||||||
|
return errors.Wrap(
|
||||||
|
pulseClient.SetSinkInputVolume(match, mute, volVal, volAbs),
|
||||||
|
"Unable to set sink input volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
case "sink":
|
||||||
|
return errors.Wrap(
|
||||||
|
pulseClient.SetSinkVolume(match, mute, volVal, volAbs),
|
||||||
|
"Unable to set sink volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
case "source":
|
||||||
|
return errors.Wrap(
|
||||||
|
pulseClient.SetSourceVolume(match, mute, volVal, volAbs),
|
||||||
|
"Unable to set source volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errors.Errorf("Unsupported device type: %q", devType)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,13 +43,13 @@ func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attribu
|
||||||
switch devType {
|
switch devType {
|
||||||
|
|
||||||
case "input":
|
case "input":
|
||||||
volume, mute, err = pulseClient.GetSinkInputVolume(match)
|
volume, mute, _, _, err = pulseClient.GetSinkInputVolume(match)
|
||||||
|
|
||||||
case "sink":
|
case "sink":
|
||||||
volume, mute, err = pulseClient.GetSinkVolume(match)
|
volume, mute, _, _, err = pulseClient.GetSinkVolume(match)
|
||||||
|
|
||||||
case "source":
|
case "source":
|
||||||
volume, mute, err = pulseClient.GetSourceVolume(match)
|
volume, mute, _, _, err = pulseClient.GetSourceVolume(match)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return errors.Errorf("Unsupported device type: %q", devType)
|
return errors.Errorf("Unsupported device type: %q", devType)
|
||||||
|
|
|
@ -38,15 +38,15 @@ func newPulseAudioClient() (*pulseAudioClient, error) {
|
||||||
|
|
||||||
func (p pulseAudioClient) Close() { p.client.Close() }
|
func (p pulseAudioClient) Close() { p.client.Close() }
|
||||||
|
|
||||||
func (p pulseAudioClient) GetSinkInputVolume(match string) (float64, bool, error) {
|
func (p pulseAudioClient) GetSinkInputVolume(match string) (vol float64, muted bool, idx uint32, max uint32, err error) {
|
||||||
m, err := regexp.Compile(match)
|
m, err := regexp.Compile(match)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, errors.Wrap(err, "Unable to compile given match RegEx")
|
return 0, false, 0, 0, errors.Wrap(err, "Unable to compile given match RegEx")
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp proto.GetSinkInputInfoListReply
|
var resp proto.GetSinkInputInfoListReply
|
||||||
if err := p.client.RawRequest(&proto.GetSinkInputInfoList{}, &resp); err != nil {
|
if err := p.client.RawRequest(&proto.GetSinkInputInfoList{}, &resp); err != nil {
|
||||||
return 0, false, errors.Wrap(err, "Unable to list sink inputs")
|
return 0, false, 0, 0, errors.Wrap(err, "Unable to list sink inputs")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, info := range resp {
|
for _, info := range resp {
|
||||||
|
@ -56,24 +56,24 @@ func (p pulseAudioClient) GetSinkInputVolume(match string) (float64, bool, error
|
||||||
|
|
||||||
sinkBase, err := p.getSinkReferenceVolumeByIndex(info.SinkIndex)
|
sinkBase, err := p.getSinkReferenceVolumeByIndex(info.SinkIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, errors.Wrap(err, "Unable to get sink base volume")
|
return 0, false, 0, 0, errors.Wrap(err, "Unable to get sink base volume")
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.unifyChannelVolumes(info.ChannelVolumes) / sinkBase, info.Muted, nil
|
return p.unifyChannelVolumes(info.ChannelVolumes) / float64(sinkBase), info.Muted, info.SinkInputIndex, sinkBase, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, false, errPulseNoSuchDevice
|
return 0, false, 0, 0, errPulseNoSuchDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p pulseAudioClient) GetSinkVolume(match string) (float64, bool, error) {
|
func (p pulseAudioClient) GetSinkVolume(match string) (vol float64, muted bool, idx uint32, max uint32, err error) {
|
||||||
m, err := regexp.Compile(match)
|
m, err := regexp.Compile(match)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, errors.Wrap(err, "Unable to compile given match RegEx")
|
return 0, false, 0, 0, errors.Wrap(err, "Unable to compile given match RegEx")
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp proto.GetSinkInfoListReply
|
var resp proto.GetSinkInfoListReply
|
||||||
if err := p.client.RawRequest(&proto.GetSinkInfoList{}, &resp); err != nil {
|
if err := p.client.RawRequest(&proto.GetSinkInfoList{}, &resp); err != nil {
|
||||||
return 0, false, errors.Wrap(err, "Unable to list sinks")
|
return 0, false, 0, 0, errors.Wrap(err, "Unable to list sinks")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, info := range resp {
|
for _, info := range resp {
|
||||||
|
@ -81,21 +81,21 @@ func (p pulseAudioClient) GetSinkVolume(match string) (float64, bool, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.unifyChannelVolumes(info.ChannelVolumes) / float64(info.NumVolumeSteps), info.Mute, nil
|
return p.unifyChannelVolumes(info.ChannelVolumes) / float64(info.NumVolumeSteps), info.Mute, info.SinkIndex, info.NumVolumeSteps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, false, errPulseNoSuchDevice
|
return 0, false, 0, 0, errPulseNoSuchDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p pulseAudioClient) GetSourceVolume(match string) (float64, bool, error) {
|
func (p pulseAudioClient) GetSourceVolume(match string) (vol float64, muted bool, idx uint32, max uint32, err error) {
|
||||||
m, err := regexp.Compile(match)
|
m, err := regexp.Compile(match)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, errors.Wrap(err, "Unable to compile given match RegEx")
|
return 0, false, 0, 0, errors.Wrap(err, "Unable to compile given match RegEx")
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp proto.GetSourceInfoListReply
|
var resp proto.GetSourceInfoListReply
|
||||||
if err := p.client.RawRequest(&proto.GetSourceInfoList{}, &resp); err != nil {
|
if err := p.client.RawRequest(&proto.GetSourceInfoList{}, &resp); err != nil {
|
||||||
return 0, false, errors.Wrap(err, "Unable to list sources")
|
return 0, false, 0, 0, errors.Wrap(err, "Unable to list sources")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, info := range resp {
|
for _, info := range resp {
|
||||||
|
@ -103,19 +103,115 @@ func (p pulseAudioClient) GetSourceVolume(match string) (float64, bool, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.unifyChannelVolumes(info.ChannelVolumes) / float64(info.NumVolumeSteps), info.Mute, nil
|
return p.unifyChannelVolumes(info.ChannelVolumes) / float64(info.NumVolumeSteps), info.Mute, info.SourceIndex, info.NumVolumeSteps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, false, errPulseNoSuchDevice
|
return 0, false, 0, 0, errPulseNoSuchDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p pulseAudioClient) getSinkReferenceVolumeByIndex(idx uint32) (float64, error) {
|
func (p pulseAudioClient) SetSinkInputVolume(match string, mute string, vol float64, absolute bool) error {
|
||||||
|
stateVol, stateMute, stateIdx, stateSteps, err := p.GetSinkInputVolume(match)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to get current state of sink input")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmds []proto.RequestArgs
|
||||||
|
|
||||||
|
switch mute {
|
||||||
|
case "true":
|
||||||
|
cmds = append(cmds, &proto.SetSinkInputMute{SinkInputIndex: stateIdx, Mute: true})
|
||||||
|
case "false":
|
||||||
|
cmds = append(cmds, &proto.SetSinkInputMute{SinkInputIndex: stateIdx, Mute: false})
|
||||||
|
case "toggle":
|
||||||
|
cmds = append(cmds, &proto.SetSinkInputMute{SinkInputIndex: stateIdx, Mute: !stateMute})
|
||||||
|
}
|
||||||
|
|
||||||
|
if absolute && vol >= 0 {
|
||||||
|
cmds = append(cmds, &proto.SetSinkInputVolume{SinkInputIndex: stateIdx, ChannelVolumes: proto.ChannelVolumes{uint32(vol * float64(stateSteps))}})
|
||||||
|
} else if vol != 0 {
|
||||||
|
cmds = append(cmds, &proto.SetSinkInputVolume{SinkInputIndex: stateIdx, ChannelVolumes: proto.ChannelVolumes{uint32(math.Max(0, stateVol+vol) * float64(stateSteps))}})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if err := p.client.RawRequest(cmd, nil); err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to execute command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pulseAudioClient) SetSinkVolume(match string, mute string, vol float64, absolute bool) error {
|
||||||
|
stateVol, stateMute, stateIdx, stateSteps, err := p.GetSinkVolume(match)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to get current state of sink")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmds []proto.RequestArgs
|
||||||
|
|
||||||
|
switch mute {
|
||||||
|
case "true":
|
||||||
|
cmds = append(cmds, &proto.SetSinkMute{SinkIndex: stateIdx, Mute: true})
|
||||||
|
case "false":
|
||||||
|
cmds = append(cmds, &proto.SetSinkMute{SinkIndex: stateIdx, Mute: false})
|
||||||
|
case "toggle":
|
||||||
|
cmds = append(cmds, &proto.SetSinkMute{SinkIndex: stateIdx, Mute: !stateMute})
|
||||||
|
}
|
||||||
|
|
||||||
|
if absolute && vol >= 0 {
|
||||||
|
cmds = append(cmds, &proto.SetSinkVolume{SinkIndex: stateIdx, ChannelVolumes: proto.ChannelVolumes{uint32(vol * float64(stateSteps))}})
|
||||||
|
} else if vol != 0 {
|
||||||
|
cmds = append(cmds, &proto.SetSinkVolume{SinkIndex: stateIdx, ChannelVolumes: proto.ChannelVolumes{uint32(math.Max(0, stateVol+vol) * float64(stateSteps))}})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if err := p.client.RawRequest(cmd, nil); err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to execute command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pulseAudioClient) SetSourceVolume(match string, mute string, vol float64, absolute bool) error {
|
||||||
|
stateVol, stateMute, stateIdx, stateSteps, err := p.GetSourceVolume(match)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to get current state of source")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmds []proto.RequestArgs
|
||||||
|
|
||||||
|
switch mute {
|
||||||
|
case "true":
|
||||||
|
cmds = append(cmds, &proto.SetSourceMute{SourceIndex: stateIdx, Mute: true})
|
||||||
|
case "false":
|
||||||
|
cmds = append(cmds, &proto.SetSourceMute{SourceIndex: stateIdx, Mute: false})
|
||||||
|
case "toggle":
|
||||||
|
cmds = append(cmds, &proto.SetSourceMute{SourceIndex: stateIdx, Mute: !stateMute})
|
||||||
|
}
|
||||||
|
|
||||||
|
if absolute && vol >= 0 {
|
||||||
|
cmds = append(cmds, &proto.SetSourceVolume{SourceIndex: stateIdx, ChannelVolumes: proto.ChannelVolumes{uint32(vol * float64(stateSteps))}})
|
||||||
|
} else if vol != 0 {
|
||||||
|
cmds = append(cmds, &proto.SetSourceVolume{SourceIndex: stateIdx, ChannelVolumes: proto.ChannelVolumes{uint32(math.Max(0, stateVol+vol) * float64(stateSteps))}})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if err := p.client.RawRequest(cmd, nil); err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to execute command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pulseAudioClient) getSinkReferenceVolumeByIndex(idx uint32) (uint32, error) {
|
||||||
var resp proto.GetSinkInfoReply
|
var resp proto.GetSinkInfoReply
|
||||||
if err := p.client.RawRequest(&proto.GetSinkInfo{SinkIndex: idx}, &resp); err != nil {
|
if err := p.client.RawRequest(&proto.GetSinkInfo{SinkIndex: idx}, &resp); err != nil {
|
||||||
return 0, errors.Wrap(err, "Unable to get sink")
|
return 0, errors.Wrap(err, "Unable to get sink")
|
||||||
}
|
}
|
||||||
|
|
||||||
return float64(resp.NumVolumeSteps), nil
|
return resp.NumVolumeSteps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p pulseAudioClient) unifyChannelVolumes(v proto.ChannelVolumes) float64 {
|
func (p pulseAudioClient) unifyChannelVolumes(v proto.ChannelVolumes) float64 {
|
||||||
|
|
Loading…
Reference in a new issue