diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/macvolumecontrol b/src/com.dlprows.macvolumecontrol.sdPlugin/macvolumecontrol new file mode 100755 index 0000000..9667cf7 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/macvolumecontrol differ diff --git a/src/keyboard/keyboard.go b/src/keyboard/keyboard.go deleted file mode 100644 index e12951d..0000000 --- a/src/keyboard/keyboard.go +++ /dev/null @@ -1,43 +0,0 @@ -package keyboard - -/* - #cgo CFLAGS: -x objective-c - #cgo LDFLAGS: -framework AppKit - #import -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 -) diff --git a/src/main.go b/src/main.go index 10892a0..6f76e73 100644 --- a/src/main.go +++ b/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"` +} diff --git a/src/volume/volume.go b/src/volume/volume.go new file mode 100644 index 0000000..de31bfd --- /dev/null +++ b/src/volume/volume.go @@ -0,0 +1,212 @@ +package volume + +/* + #cgo CFLAGS: -x objective-c + #cgo LDFLAGS: -framework AppKit + #import +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()