Compare commits
4 Commits
74a9482dbc
...
f879a49e7d
Author | SHA1 | Date | |
---|---|---|---|
|
f879a49e7d | ||
|
ac1f672891 | ||
|
45520c9513 | ||
|
259ef4a3d2 |
@ -1,4 +1,4 @@
|
||||
INSTALLDIR = "~/Library/Application\ Support/com.elgato.StreamDeck/Plugins/com.dlprows.macvolumecontrol.sdPlugin"
|
||||
INSTALLDIR = ~/Library/Application\ Support/com.elgato.StreamDeck/Plugins/com.dlprows.macvolumecontrol.sdPlugin
|
||||
#BUILDDIR = build
|
||||
#RELEASEDIR = release
|
||||
#SDPLUGINDIR = "./com.dlprows.macvolumecontrol.sdPlugin"
|
||||
|
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
|
||||
)
|
@ -20,8 +20,9 @@ func directory() string {
|
||||
// Enable turns logging on for the StreamDeck API client as well as the global
|
||||
// log object. It sends both to a temp file that contains <project_directory>.log.
|
||||
func Enable() {
|
||||
os.Mkdir("./logs/", 0775)
|
||||
d := directory()
|
||||
f, err := os.CreateTemp("./", fmt.Sprintf("%s.log", d))
|
||||
f, err := os.CreateTemp("./logs/", fmt.Sprintf("%s.log", d))
|
||||
if err != nil {
|
||||
log.Fatalf("error creating temp file: %v", err)
|
||||
}
|
||||
|
147
src/main.go
147
src/main.go
@ -3,16 +3,20 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"macvolumecontrol/keyboard"
|
||||
"macvolumecontrol/logging"
|
||||
"macvolumecontrol/volume"
|
||||
|
||||
"code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk"
|
||||
sdcontext "code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk/context"
|
||||
)
|
||||
|
||||
var _currentSettings *volume.VolumeSettings = &volume.VolumeSettings{}
|
||||
|
||||
func main() {
|
||||
logging.Enable()
|
||||
log.Println("Starting plugin")
|
||||
@ -36,46 +40,62 @@ func setup(client *streamdeck.Client) {
|
||||
log.Println("Registering actions")
|
||||
action := client.Action("com.dlprows.macvolumecontrol.dialaction")
|
||||
|
||||
debounce := time.Now()
|
||||
contexts := make(map[string]struct{})
|
||||
|
||||
action.RegisterHandler(streamdeck.DialRotate, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
|
||||
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]{}
|
||||
|
||||
if err := json.Unmarshal(event.Payload, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Ticks > 0 {
|
||||
keyboard.PressMediaKey(keyboard.VOLUMEUP)
|
||||
} else {
|
||||
keyboard.PressMediaKey(keyboard.VOLUMEDOWN)
|
||||
//volume.ChangeVolumeWithKeyboard(p.Ticks)
|
||||
newSettings, err := volume.ChangeVolume(p.Ticks)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
log.Println("Will Appear")
|
||||
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 {
|
||||
@ -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
|
||||
//go func() {
|
||||
go func() {
|
||||
for range time.Tick(time.Second * 1) {
|
||||
newSettings, err := volume.GetVolumeSettings()
|
||||
|
||||
//}()
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
/*
|
||||
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'
|
||||
*/
|
||||
|
||||
/*
|
||||
for ctxStr := range contexts {
|
||||
//for each context
|
||||
//build a new context that can be used to perform outbound requests
|
||||
for ctxStr := range contexts {
|
||||
//for each context
|
||||
//build a new context that can be used to perform outbound requests
|
||||
ctx := context.Background()
|
||||
ctx = sdcontext.WithContext(ctx, ctxStr)
|
||||
|
||||
img, err := streamdeck.Image(graph(readings))
|
||||
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
|
||||
}
|
||||
setFeedbackIfNeeded(ctx, client, newSettings)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
134
src/volume/coreAudio.go
Normal file
134
src/volume/coreAudio.go
Normal file
@ -0,0 +1,134 @@
|
||||
package volume
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework CoreAudio
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type OSStatus C.OSStatus
|
||||
|
||||
const (
|
||||
AudioHardwareNoError = C.kAudioHardwareNoError
|
||||
)
|
||||
|
||||
type AudioObjectID uint32
|
||||
|
||||
func GetDefaultOutputDevice() (AudioObjectID, OSStatus) {
|
||||
properties := C.AudioObjectPropertyAddress{
|
||||
C.kAudioHardwarePropertyDefaultOutputDevice,
|
||||
C.kAudioObjectPropertyScopeGlobal,
|
||||
C.kAudioObjectPropertyElementMain,
|
||||
}
|
||||
|
||||
var data C.AudioObjectID
|
||||
dataSize := C.UInt32(unsafe.Sizeof(data))
|
||||
|
||||
result := C.AudioObjectGetPropertyData(
|
||||
C.kAudioObjectSystemObject,
|
||||
&properties,
|
||||
C.UInt32(0),
|
||||
unsafe.Pointer(nil),
|
||||
&dataSize,
|
||||
unsafe.Pointer(&data),
|
||||
)
|
||||
|
||||
return AudioObjectID(data), OSStatus(result)
|
||||
}
|
||||
|
||||
func GetVolume(objectID AudioObjectID) (float32, OSStatus) {
|
||||
|
||||
properties := C.AudioObjectPropertyAddress{
|
||||
C.kAudioDevicePropertyVolumeScalar,
|
||||
C.kAudioDevicePropertyScopeOutput,
|
||||
0, //main channel
|
||||
}
|
||||
|
||||
var data C.Float32
|
||||
dataSize := C.UInt32(unsafe.Sizeof(data))
|
||||
|
||||
result := C.AudioObjectGetPropertyData(
|
||||
C.AudioObjectID(objectID),
|
||||
&properties,
|
||||
C.UInt32(0),
|
||||
unsafe.Pointer(nil),
|
||||
&dataSize,
|
||||
unsafe.Pointer(&data),
|
||||
)
|
||||
|
||||
return float32(data), OSStatus(result)
|
||||
}
|
||||
|
||||
func SetVolume(objectID AudioObjectID, volume float32) OSStatus {
|
||||
|
||||
properties := C.AudioObjectPropertyAddress{
|
||||
C.kAudioDevicePropertyVolumeScalar,
|
||||
C.kAudioDevicePropertyScopeOutput,
|
||||
0, //main channel
|
||||
}
|
||||
|
||||
data := C.Float32(volume)
|
||||
|
||||
result := C.AudioObjectSetPropertyData(
|
||||
C.AudioObjectID(objectID),
|
||||
&properties,
|
||||
C.UInt32(0),
|
||||
unsafe.Pointer(nil),
|
||||
C.UInt32(unsafe.Sizeof(data)),
|
||||
unsafe.Pointer(&data),
|
||||
)
|
||||
|
||||
return OSStatus(result)
|
||||
}
|
||||
|
||||
func GetMute(objectID AudioObjectID) (bool, OSStatus) {
|
||||
|
||||
properties := C.AudioObjectPropertyAddress{
|
||||
C.kAudioDevicePropertyMute,
|
||||
C.kAudioDevicePropertyScopeOutput,
|
||||
0, //main channel
|
||||
}
|
||||
|
||||
var data C.UInt32
|
||||
dataSize := C.UInt32(unsafe.Sizeof(data))
|
||||
|
||||
result := C.AudioObjectGetPropertyData(
|
||||
C.AudioObjectID(objectID),
|
||||
&properties,
|
||||
C.UInt32(0),
|
||||
unsafe.Pointer(nil),
|
||||
&dataSize,
|
||||
unsafe.Pointer(&data),
|
||||
)
|
||||
|
||||
dataAsBool := uint32(data) != 0
|
||||
return dataAsBool, OSStatus(result)
|
||||
}
|
||||
|
||||
func SetMute(objectID AudioObjectID, mute bool) OSStatus {
|
||||
|
||||
properties := C.AudioObjectPropertyAddress{
|
||||
C.kAudioDevicePropertyMute,
|
||||
C.kAudioDevicePropertyScopeOutput,
|
||||
0, //main channel
|
||||
}
|
||||
|
||||
data := C.UInt32(0)
|
||||
if mute {
|
||||
data = C.UInt32(1)
|
||||
}
|
||||
|
||||
result := C.AudioObjectSetPropertyData(
|
||||
C.AudioObjectID(objectID),
|
||||
&properties,
|
||||
C.UInt32(0),
|
||||
unsafe.Pointer(nil),
|
||||
C.UInt32(unsafe.Sizeof(data)),
|
||||
unsafe.Pointer(&data),
|
||||
)
|
||||
return OSStatus(result)
|
||||
}
|
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