forked from omriharel/deej
/
util.go
126 lines (102 loc) · 3.64 KB
/
util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package util
import (
"fmt"
"math"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
"go.uber.org/zap"
)
// EnsureDirExists creates the given directory path if it doesn't already exist
func EnsureDirExists(path string) error {
if err := os.MkdirAll(path, os.ModePerm); err != nil {
return fmt.Errorf("ensure directory exists (%s): %w", path, err)
}
return nil
}
// FileExists checks if a file exists and is not a directory before we
// try using it to prevent further errors.
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// Linux returns true if we're running on Linux
func Linux() bool {
return runtime.GOOS == "linux"
}
// SetupCloseHandler creates a 'listener' on a new goroutine which will notify the
// program if it receives an interrupt from the OS
func SetupCloseHandler() chan os.Signal {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
return c
}
// GetCurrentWindowProcessNames returns the process names (including extension, if applicable)
// of the current foreground window. This includes child processes belonging to the window.
// This is currently only implemented for Windows
func GetCurrentWindowProcessNames() ([]string, error) {
return getCurrentWindowProcessNames()
}
// OpenExternal spawns a detached window with the provided command and argument
func OpenExternal(logger *zap.SugaredLogger, cmd string, arg string) error {
// use cmd for windows, bash for linux
execCommandArgs := []string{"cmd.exe", "/C", "start", "/b", cmd, arg}
if Linux() {
execCommandArgs = []string{"/bin/bash", "-c", fmt.Sprintf("%s %s", cmd, arg)}
}
command := exec.Command(execCommandArgs[0], execCommandArgs[1:]...)
if err := command.Run(); err != nil {
logger.Warnw("Failed to spawn detached process",
"command", cmd,
"argument", arg,
"error", err)
return fmt.Errorf("spawn detached proc: %w", err)
}
return nil
}
// NormalizeScalar "trims" the given float32 to 2 points of precision (e.g. 0.15442 -> 0.15)
// This is used both for windows core audio volume levels and for cleaning up slider level values from serial
func NormalizeScalar(v float32) float32 {
return float32(math.Floor(float64(v)*100) / 100.0)
}
// SignificantlyDifferent returns true if there's a significant enough volume difference between two given values
func SignificantlyDifferent(old float32, new float32, noiseReductionLevel string) bool {
const (
noiseReductionHigh = "high"
noiseReductionLow = "low"
)
// this threshold is solely responsible for dealing with hardware interference when
// sliders are producing noisy values. this value should be a median value between two
// round percent values. for instance, 0.025 means volume can move at 3% increments
var significantDifferenceThreshold float64
// choose our noise reduction level based on the config-provided value
switch noiseReductionLevel {
case noiseReductionHigh:
significantDifferenceThreshold = 0.035
break
case noiseReductionLow:
significantDifferenceThreshold = 0.015
break
default:
significantDifferenceThreshold = 0.025
break
}
if math.Abs(float64(old-new)) >= significantDifferenceThreshold {
return true
}
// special behavior is needed around the edges of 0.0 and 1.0 - this makes it snap (just a tiny bit) to them
if (almostEquals(new, 1.0) && old != 1.0) || (almostEquals(new, 0.0) && old != 0.0) {
return true
}
// values are close enough to not warrant any action
return false
}
// a helper to make sure volume snaps correctly to 0 and 100, where appropriate
func almostEquals(a float32, b float32) bool {
return math.Abs(float64(a-b)) < 0.000001
}