Implement UI feedback, polling, and the underlying code can support keyboard or core audio for changes to volume
This commit is contained in:
parent
ac1f672891
commit
f879a49e7d
BIN
src/com.dlprows.macvolumecontrol.sdPlugin/macvolumecontrol
Executable file
BIN
src/com.dlprows.macvolumecontrol.sdPlugin/macvolumecontrol
Executable file
Binary file not shown.
@ -1,43 +0,0 @@
|
|||||||
package keyboard
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo CFLAGS: -x objective-c
|
|
||||||
#cgo LDFLAGS: -framework AppKit
|
|
||||||
#import <AppKit/AppKit.h>
|
|
||||||
NSEvent* createEvent(int k, bool down) {
|
|
||||||
int flags = down ? 0xa00 : 0xb00;
|
|
||||||
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeSystemDefined
|
|
||||||
location:NSZeroPoint
|
|
||||||
modifierFlags:flags
|
|
||||||
timestamp:0
|
|
||||||
windowNumber:0
|
|
||||||
context:nil
|
|
||||||
subtype:8
|
|
||||||
data1:(k << 16 | flags)
|
|
||||||
data2:-1
|
|
||||||
];
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
void pressKey(int k) {
|
|
||||||
@autoreleasepool {
|
|
||||||
NSEvent* down = createEvent(k, true);
|
|
||||||
NSEvent* up = createEvent(k, false);
|
|
||||||
|
|
||||||
CGEventPost(kCGHIDEventTap, down.CGEvent);
|
|
||||||
usleep(1000);
|
|
||||||
CGEventPost(kCGHIDEventTap, up.CGEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
func PressMediaKey(key int) {
|
|
||||||
C.pressKey(C.int(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
VOLUMEUP = 0
|
|
||||||
VOLUMEDOWN = 1
|
|
||||||
MUTE = 7
|
|
||||||
)
|
|
147
src/main.go
147
src/main.go
@ -3,16 +3,20 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"macvolumecontrol/keyboard"
|
|
||||||
"macvolumecontrol/logging"
|
"macvolumecontrol/logging"
|
||||||
|
"macvolumecontrol/volume"
|
||||||
|
|
||||||
"code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk"
|
"code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk"
|
||||||
|
sdcontext "code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _currentSettings *volume.VolumeSettings = &volume.VolumeSettings{}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
logging.Enable()
|
logging.Enable()
|
||||||
log.Println("Starting plugin")
|
log.Println("Starting plugin")
|
||||||
@ -36,46 +40,62 @@ func setup(client *streamdeck.Client) {
|
|||||||
log.Println("Registering actions")
|
log.Println("Registering actions")
|
||||||
action := client.Action("com.dlprows.macvolumecontrol.dialaction")
|
action := client.Action("com.dlprows.macvolumecontrol.dialaction")
|
||||||
|
|
||||||
debounce := time.Now()
|
|
||||||
contexts := make(map[string]struct{})
|
contexts := make(map[string]struct{})
|
||||||
|
|
||||||
action.RegisterHandler(streamdeck.DialRotate, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
action.RegisterHandler(streamdeck.DialRotate, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
||||||
log.Println("dial rotate")
|
log.Println("dial rotate")
|
||||||
|
|
||||||
t := time.Now()
|
|
||||||
elapsed := t.Sub(debounce)
|
|
||||||
|
|
||||||
if elapsed.Milliseconds() < 50 {
|
|
||||||
log.Println("dial rotate skipped")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
debounce = t
|
|
||||||
|
|
||||||
p := streamdeck.DialRotatePayload[any]{}
|
p := streamdeck.DialRotatePayload[any]{}
|
||||||
|
|
||||||
if err := json.Unmarshal(event.Payload, &p); err != nil {
|
if err := json.Unmarshal(event.Payload, &p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Ticks > 0 {
|
//volume.ChangeVolumeWithKeyboard(p.Ticks)
|
||||||
keyboard.PressMediaKey(keyboard.VOLUMEUP)
|
newSettings, err := volume.ChangeVolume(p.Ticks)
|
||||||
} else {
|
|
||||||
keyboard.PressMediaKey(keyboard.VOLUMEDOWN)
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return setFeedbackIfNeeded(ctx, client, newSettings)
|
||||||
})
|
})
|
||||||
|
|
||||||
action.RegisterHandler(streamdeck.DialDown, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
action.RegisterHandler(streamdeck.DialDown, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
||||||
log.Println("dial down")
|
log.Println("dial down")
|
||||||
keyboard.PressMediaKey(keyboard.MUTE)
|
|
||||||
return nil
|
newSettings, err := volume.ToggleMute()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return setFeedbackIfNeeded(ctx, client, newSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
action.RegisterHandler(streamdeck.TouchTap, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
||||||
|
log.Println("touch tap")
|
||||||
|
|
||||||
|
newSettings, err := volume.ToggleMute()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return setFeedbackIfNeeded(ctx, client, newSettings)
|
||||||
})
|
})
|
||||||
|
|
||||||
action.RegisterHandler(streamdeck.WillAppear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
action.RegisterHandler(streamdeck.WillAppear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
||||||
log.Println("Will Appear")
|
log.Println("Will Appear")
|
||||||
contexts[event.Context] = struct{}{}
|
contexts[event.Context] = struct{}{}
|
||||||
return nil
|
|
||||||
|
newSettings, err := volume.GetVolumeSettings()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return setFeedbackIfNeeded(ctx, client, newSettings)
|
||||||
})
|
})
|
||||||
|
|
||||||
action.RegisterHandler(streamdeck.WillDisappear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
action.RegisterHandler(streamdeck.WillDisappear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
||||||
@ -85,44 +105,63 @@ func setup(client *streamdeck.Client) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//start background thread to keep the display up to date if changed outside the stream deck
|
//start background thread to keep the display up to date if changed outside the stream deck
|
||||||
//go func() {
|
go func() {
|
||||||
|
for range time.Tick(time.Second * 1) {
|
||||||
|
newSettings, err := volume.GetVolumeSettings()
|
||||||
|
|
||||||
//}()
|
if err != nil {
|
||||||
}
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
for ctxStr := range contexts {
|
||||||
osascript -e 'output volume of (get volume settings)'
|
//for each context
|
||||||
osascript -e 'set volume output volume 50'
|
//build a new context that can be used to perform outbound requests
|
||||||
osascript -e 'output muted of (get volume settings)'
|
|
||||||
osascript -e 'set volume output muted true'
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
for ctxStr := range contexts {
|
|
||||||
//for each context
|
|
||||||
//build a new context that can be used to perform outbound requests
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = sdcontext.WithContext(ctx, ctxStr)
|
ctx = sdcontext.WithContext(ctx, ctxStr)
|
||||||
|
|
||||||
img, err := streamdeck.Image(graph(readings))
|
setFeedbackIfNeeded(ctx, client, newSettings)
|
||||||
if err != nil {
|
|
||||||
log.Printf("error creating image: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.SetImage(ctx, img, streamdeck.HardwareAndSoftware); err != nil {
|
|
||||||
log.Printf("error setting image: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
title := ""
|
|
||||||
if pi.ShowText {
|
|
||||||
title = fmt.Sprintf("CPU\n%d%%", int(r[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.SetTitle(ctx, title, streamdeck.HardwareAndSoftware); err != nil {
|
|
||||||
log.Printf("error setting title: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFeedbackIfNeeded(ctx context.Context, client *streamdeck.Client, newSettings *volume.VolumeSettings) error {
|
||||||
|
|
||||||
|
if _currentSettings.OutputVolume == newSettings.OutputVolume && _currentSettings.OutputMuted == newSettings.OutputMuted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := FeedbackPayload{}
|
||||||
|
|
||||||
|
opacity := 1.0
|
||||||
|
|
||||||
|
if newSettings.OutputMuted {
|
||||||
|
opacity = 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.Value = ValueWithOpacity[string]{
|
||||||
|
fmt.Sprintf("%d%%", newSettings.OutputVolume),
|
||||||
|
opacity,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.Indicator = ValueWithOpacity[int]{
|
||||||
|
newSettings.OutputVolume,
|
||||||
|
opacity,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.Icon = ValueWithOpacity[any]{nil, opacity}
|
||||||
|
|
||||||
|
_currentSettings = newSettings
|
||||||
|
return client.SetFeedback(ctx, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedbackPayload struct {
|
||||||
|
Value ValueWithOpacity[string] `json:"value"`
|
||||||
|
Indicator ValueWithOpacity[int] `json:"indicator"`
|
||||||
|
Icon ValueWithOpacity[any] `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueWithOpacity[T any] struct {
|
||||||
|
Value T `json:"value,omitempty"`
|
||||||
|
Opacity float64 `json:"opacity"`
|
||||||
|
}
|
||||||
|
212
src/volume/volume.go
Normal file
212
src/volume/volume.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package volume
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -x objective-c
|
||||||
|
#cgo LDFLAGS: -framework AppKit
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
NSEvent* createEvent(int k, bool down) {
|
||||||
|
int flags = down ? 0xa00 : 0xb00;
|
||||||
|
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeSystemDefined
|
||||||
|
location:NSZeroPoint
|
||||||
|
modifierFlags:flags
|
||||||
|
timestamp:0
|
||||||
|
windowNumber:0
|
||||||
|
context:nil
|
||||||
|
subtype:8
|
||||||
|
data1:(k << 16 | flags)
|
||||||
|
data2:-1
|
||||||
|
];
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pressKey(int k) {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSEvent* down = createEvent(k, true);
|
||||||
|
NSEvent* up = createEvent(k, false);
|
||||||
|
|
||||||
|
CGEventPost(kCGHIDEventTap, down.CGEvent);
|
||||||
|
usleep(100);
|
||||||
|
CGEventPost(kCGHIDEventTap, up.CGEvent);
|
||||||
|
usleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
osascript -e 'output volume of (get volume settings)'
|
||||||
|
osascript -e 'set volume output volume 50'
|
||||||
|
osascript -e 'output muted of (get volume settings)'
|
||||||
|
osascript -e 'set volume output muted true'
|
||||||
|
*/
|
||||||
|
type VolumeSettings struct {
|
||||||
|
OutputVolume int
|
||||||
|
OutputMuted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVolumeSettings() (*VolumeSettings, error) {
|
||||||
|
//osascript -e "get volume settings"
|
||||||
|
//output volume:81, input volume:50, alert volume:100, output muted:false
|
||||||
|
|
||||||
|
device, result := GetDefaultOutputDevice()
|
||||||
|
|
||||||
|
if result != AudioHardwareNoError {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Unable to get device: %d", result))
|
||||||
|
}
|
||||||
|
|
||||||
|
return getVolumeSettings(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVolumeSettings(device AudioObjectID) (*VolumeSettings, error) {
|
||||||
|
|
||||||
|
volume, result := GetVolume(device)
|
||||||
|
if result != AudioHardwareNoError {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Unable to get volume: %d", result))
|
||||||
|
}
|
||||||
|
|
||||||
|
muted, result := GetMute(device)
|
||||||
|
if result != AudioHardwareNoError {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Unable to get mute: %d", result))
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := VolumeSettings{
|
||||||
|
int(volume * 100),
|
||||||
|
muted,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeVolume(ticks int) (*VolumeSettings, error) {
|
||||||
|
|
||||||
|
device, result := GetDefaultOutputDevice()
|
||||||
|
|
||||||
|
if result != AudioHardwareNoError {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Unable to get device: %d", result))
|
||||||
|
}
|
||||||
|
|
||||||
|
volume, result := GetVolume(device)
|
||||||
|
if result != AudioHardwareNoError {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Unable to get volume: %d", result))
|
||||||
|
}
|
||||||
|
|
||||||
|
volume = volume * 64 //convert from fractional volume to 64 steps that mac supports
|
||||||
|
volume += float32(ticks) //add number of ticks
|
||||||
|
volume = volume / 64 //convert back to fractional volume that mac supports
|
||||||
|
|
||||||
|
SetVolume(device, volume)
|
||||||
|
|
||||||
|
return getVolumeSettings(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToggleMute() (*VolumeSettings, error) {
|
||||||
|
device, result := GetDefaultOutputDevice()
|
||||||
|
|
||||||
|
if result != AudioHardwareNoError {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Unable to get device: %d", result))
|
||||||
|
}
|
||||||
|
|
||||||
|
muted, result := GetMute(device)
|
||||||
|
if result != AudioHardwareNoError {
|
||||||
|
return nil, errors.New(fmt.Sprintf("Unable to get volume: %d", result))
|
||||||
|
}
|
||||||
|
|
||||||
|
SetMute(device, !muted)
|
||||||
|
|
||||||
|
return getVolumeSettings(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeVolumeWithKeyboard(ticks int) {
|
||||||
|
key := volumeup
|
||||||
|
|
||||||
|
if ticks < 0 {
|
||||||
|
key = volumedown
|
||||||
|
ticks = -1 * ticks
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 0; x < ticks; x++ {
|
||||||
|
C.pressKey(C.int(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToggleMuteWithKeyboard() {
|
||||||
|
C.pressKey(C.int(mute))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
volumeup = 0
|
||||||
|
volumedown = 1
|
||||||
|
mute = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get volume in applescript
|
||||||
|
out, err := exec.Command("osascript", "-e", "get volume settings").Output()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
split1 := strings.Split(string(out), ",")
|
||||||
|
|
||||||
|
settings := VolumeSettings{}
|
||||||
|
|
||||||
|
for _, sub := range split1 {
|
||||||
|
|
||||||
|
if strings.HasPrefix(sub, "output volume:") {
|
||||||
|
outputVolume, err := strconv.Atoi(strings.Split(sub, ":")[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.OutputVolume = outputVolume
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(sub, "output muted:") {
|
||||||
|
|
||||||
|
outputMuted, err := strconv.ParseBool(strings.Split(sub, ":")[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.OutputMuted = outputMuted
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
set volume in applescript
|
||||||
|
|
||||||
|
//script := "set volume output muted (not output muted of (get volume settings))"
|
||||||
|
settings, err := GetVolumeSettings()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
volume := float64(settings.OutputVolume) * 64 / 100
|
||||||
|
volume += float64(ticks)
|
||||||
|
newVolume := int(math.Round(volume * 100 / 64))
|
||||||
|
|
||||||
|
if newVolume > 100 {
|
||||||
|
newVolume = 100
|
||||||
|
}
|
||||||
|
if newVolume < 0 {
|
||||||
|
newVolume = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.OutputVolume = newVolume
|
||||||
|
|
||||||
|
script := fmt.Sprintf("set volume output volume %d", newVolume)
|
||||||
|
|
||||||
|
return settings, exec.Command("osascript", "-e", script).Run()
|
||||||
|
*/
|
||||||
|
|
||||||
|
//script := "set volume output muted (not output muted of (get volume settings))"
|
||||||
|
//return exec.Command("osascript", "-e", "set volume output muted (not output muted of (get volume settings))").Run()
|
Loading…
Reference in New Issue
Block a user