1
0
Fork 0
mirror of https://github.com/Luzifer/streamdeck.git synced 2024-12-20 17:51:21 +00:00

Add pulseaudio volume display

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2020-08-09 15:30:06 +02:00
parent 862c9ec7c4
commit 661edb591a
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
4 changed files with 240 additions and 0 deletions

View file

@ -0,0 +1,125 @@
// +build linux
package main
import (
"context"
"fmt"
"image/color"
"time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
func init() {
registerDisplayElement("pulsevolume", &displayElementPulseVolume{})
}
type displayElementPulseVolume struct{}
func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attributes map[string]interface{}) error {
if pulseClient == nil {
return errors.New("PulseAudio client not initialized")
}
sinkMatch, sinkOK := attributes["sink"].(string)
sinkOK = sinkOK && sinkMatch != ""
sinkInputMatch, sinkInputOK := attributes["sink_input"].(string)
sinkInputOK = sinkInputOK && sinkInputMatch != ""
var (
err error
volume float64
)
switch {
case (sinkInputOK && sinkOK) || (!sinkInputOK && !sinkOK):
return errors.New("Exactly one of 'sink' and 'sink_input' must be specified")
case sinkInputOK:
volume, err = pulseClient.GetSinkInputVolume(sinkInputMatch)
case sinkOK:
volume, err = pulseClient.GetSinkVolume(sinkMatch)
}
if err != nil {
return errors.Wrap(err, "Unable to get volume")
}
img := newTextOnImageRenderer()
// Initialize color
var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff}
if rgba, ok := attributes["color"].([]interface{}); ok {
if len(rgba) != 4 {
return errors.New("RGBA color definition needs 4 hex values")
}
tmpCol := color.RGBA{}
for cidx, vp := range []*uint8{&tmpCol.R, &tmpCol.G, &tmpCol.B, &tmpCol.A} {
switch rgba[cidx].(type) {
case int:
*vp = uint8(rgba[cidx].(int))
case float64:
*vp = uint8(rgba[cidx].(float64))
}
}
textColor = tmpCol
}
// Initialize fontsize
var fontsize float64 = 120
if v, ok := attributes["font_size"].(float64); ok {
fontsize = v
}
var border = 10
if v, ok := attributes["border"].(int); ok {
border = v
}
if err = img.DrawBigText(fmt.Sprintf("%.0f%%", volume*100), fontsize, border, textColor); err != nil {
return errors.Wrap(err, "Unable to draw text")
}
if err = ctx.Err(); err != nil {
// Page context was cancelled, do not draw
return err
}
return errors.Wrap(sd.FillImage(idx, img.GetImage()), "Unable to set image")
}
func (d displayElementPulseVolume) NeedsLoop(attributes map[string]interface{}) bool { return true }
func (d *displayElementPulseVolume) StartLoopDisplay(ctx context.Context, idx int, attributes map[string]interface{}) error {
var interval = time.Second
if v, ok := attributes["interval"].(int); ok {
interval = time.Duration(v) * time.Second
}
go func() {
for tick := time.NewTicker(interval); ; <-tick.C {
if ctx.Err() != nil {
return
}
if err := d.Display(ctx, idx, attributes); err != nil {
log.WithError(err).Error("Unable to refresh element")
}
}
}()
return nil
}
func (d *displayElementPulseVolume) StopLoopDisplay() error {
return nil
}

View file

@ -10,6 +10,7 @@ require (
github.com/Luzifer/streamdeck v0.0.0-20191122003228-a2f524a6b22c
github.com/fsnotify/fsnotify v1.4.7
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/pkg/errors v0.8.1

View file

@ -11,6 +11,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214 h1:2xVJKIumEUWeV3vczQwn61SHjNZ94Bwk+4CTjmcePxk=
github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=

View file

@ -0,0 +1,112 @@
// +build linux
package main
import (
"math"
"regexp"
"github.com/jfreymuth/pulse"
"github.com/jfreymuth/pulse/proto"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
var pulseClient *pulseAudioClient
func init() {
var err error
if pulseClient, err = newPulseAudioClient(); err != nil {
log.WithError(err).Error("Unable to connect to PulseAudio, functionality will not work")
}
}
type pulseAudioClient struct {
client *pulse.Client
}
func newPulseAudioClient() (*pulseAudioClient, error) {
c, err := pulse.NewClient()
if err != nil {
return nil, errors.Wrap(err, "Unable to create pulse client")
}
return &pulseAudioClient{client: c}, nil
}
func (p pulseAudioClient) Close() { p.client.Close() }
func (p pulseAudioClient) GetSinkInputVolume(match string) (float64, error) {
m, err := regexp.Compile(match)
if err != nil {
return 0, errors.Wrap(err, "Unable to compile given match RegEx")
}
var resp proto.GetSinkInputInfoListReply
if err := p.client.RawRequest(&proto.GetSinkInputInfoList{}, &resp); err != nil {
return 0, errors.Wrap(err, "Unable to list sink inputs")
}
for _, info := range resp {
if !m.MatchString(info.MediaName) && !m.Match(info.Properties["application.name"]) {
continue
}
sinkBase, err := p.getSinkBaseVolumeByIndex(info.SinkIndex)
if err != nil {
return 0, errors.Wrap(err, "Unable to get sink base volume")
}
return p.unifyChannelVolumes(info.ChannelVolumes) / sinkBase, nil
}
return 0, errors.New("No such sink")
}
func (p pulseAudioClient) GetSinkVolume(match string) (float64, error) {
m, err := regexp.Compile(match)
if err != nil {
return 0, errors.Wrap(err, "Unable to compile given match RegEx")
}
var resp proto.GetSinkInfoListReply
if err := p.client.RawRequest(&proto.GetSinkInfoList{}, &resp); err != nil {
return 0, errors.Wrap(err, "Unable to list sinks")
}
for _, info := range resp {
if !m.MatchString(info.SinkName) && !m.MatchString(info.Device) {
continue
}
return p.unifyChannelVolumes(info.ChannelVolumes) / float64(info.BaseVolume), nil
}
return 0, errors.New("No such sink")
}
func (p pulseAudioClient) getSinkBaseVolumeByIndex(idx uint32) (float64, error) {
var resp proto.GetSinkInfoReply
if err := p.client.RawRequest(&proto.GetSinkInfo{SinkIndex: idx}, &resp); err != nil {
return 0, errors.Wrap(err, "Unable to get sink")
}
return float64(resp.BaseVolume), nil
}
func (p pulseAudioClient) unifyChannelVolumes(v proto.ChannelVolumes) float64 {
if len(v) == 0 {
return 0
}
if len(v) == 1 {
return float64(v[0])
}
var vMin = float64(v[0])
for i := 1; i < len(v); i++ {
vMin = math.Min(vMin, float64(v[i]))
}
return vMin
}