From 9d16f4c53474be3564e2a3f706bdd31d261ec973 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Wed, 22 Jun 2022 20:51:25 +0100 Subject: [PATCH] initial commit interface code + a few tests --- README.md | 16 ++- go.mod | 5 + go.sum | 2 + tests/higher_test.go | 51 ++++++++ voicemeeter/base.go | 264 +++++++++++++++++++++++++++++++++++++++++ voicemeeter/bus.go | 44 +++++++ voicemeeter/button.go | 64 ++++++++++ voicemeeter/cdll.go | 56 +++++++++ voicemeeter/channel.go | 65 ++++++++++ voicemeeter/kinds.go | 44 +++++++ voicemeeter/remote.go | 77 ++++++++++++ voicemeeter/strip.go | 73 ++++++++++++ 12 files changed, 759 insertions(+), 2 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 tests/higher_test.go create mode 100644 voicemeeter/base.go create mode 100644 voicemeeter/bus.go create mode 100644 voicemeeter/button.go create mode 100644 voicemeeter/cdll.go create mode 100644 voicemeeter/channel.go create mode 100644 voicemeeter/kinds.go create mode 100644 voicemeeter/remote.go create mode 100644 voicemeeter/strip.go diff --git a/README.md b/README.md index 687222d..d358eb8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ -# voicemeeter-api-go -A Go wrapper for the Voiceemeter API +# A Go Wrapper for Voicemeeter API + +A WIP... + +## Tested against + +- Basic 1.0.8.2 +- Banana 2.0.6.2 +- Potato 3.0.2.2 + +## Requirements + +- [Voicemeeter](https://voicemeeter.com/) +- Go 1.18 or greater diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..866e505 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/onyx-and-iris/voicemeeter-api-go + +go 1.18 + +require golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..61b31c2 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/tests/higher_test.go b/tests/higher_test.go new file mode 100644 index 0000000..ec5843d --- /dev/null +++ b/tests/higher_test.go @@ -0,0 +1,51 @@ +package voicemeeter_test + +import ( + "os" + "testing" + + "github.com/onyx-and-iris/voicemeeter-api-go/voicemeeter" +) + +var ( + vmRem = voicemeeter.NewRemote("banana") +) + +func TestMain(m *testing.M) { + vmRem.Login() + code := m.Run() + vmRem.Logout() + os.Exit(code) +} + +func TestStrip0Mute(t *testing.T) { + //t.Skip("skipping test") + vmRem.Strip[0].SetMute(true) + if vmRem.Strip[0].GetMute() != true { + t.Error("TestStrip0Mute did not match true") + } +} + +func TestStrip2Limit(t *testing.T) { + //t.Skip("skipping test") + vmRem.Strip[2].SetLimit(-8) + if vmRem.Strip[2].GetLimit() != -8 { + t.Error("TestStrip3Limit did not match -8") + } +} + +func TestStrip4Label(t *testing.T) { + //t.Skip("skipping test") + vmRem.Strip[4].SetLabel("test0") + if vmRem.Strip[4].GetLabel() != "test0" { + t.Error("TestStrip4Label did not match test0") + } +} + +func TestStrip5Gain(t *testing.T) { + //t.Skip("skipping test") + vmRem.Strip[4].SetGain(-20.8) + if vmRem.Strip[4].GetGain() != -20.8 { + t.Error("TestStrip5Gain did not match -20.8") + } +} diff --git a/voicemeeter/base.go b/voicemeeter/base.go new file mode 100644 index 0000000..89b3f1c --- /dev/null +++ b/voicemeeter/base.go @@ -0,0 +1,264 @@ +package voicemeeter + +import ( + "bytes" + "fmt" + "math" + "os" + "syscall" + "time" + "unsafe" +) + +var ( + mod = syscall.NewLazyDLL(getDllPath()) + + vmLogin = mod.NewProc("VBVMR_Login") + vmLogout = mod.NewProc("VBVMR_Logout") + vmRunvm = mod.NewProc("VBVMR_RunVoicemeeter") + vmGetvmType = mod.NewProc("VBVMR_GetVoicemeeterType") + vmGetvmVersion = mod.NewProc("VBVMR_GetVoicemeeterVersion") + vmPdirty = mod.NewProc("VBVMR_IsParametersDirty") + getParamFloat = mod.NewProc("VBVMR_GetParameterFloat") + getParamString = mod.NewProc("VBVMR_GetParameterStringA") + + //getLevelFloat = mod.NewProc("VBVMR_GetLevel") + + setParamFloat = mod.NewProc("VBVMR_SetParameterFloat") + setParameters = mod.NewProc("VBVMR_SetParameters") + setParamString = mod.NewProc("VBVMR_SetParameterStringA") + + //getDevNumOut = mod.NewProc("VBVMR_Output_GetDeviceNumber") + //getDevDescOut = mod.NewProc("VBVMR_Output_GetDeviceDescA") + //getDevNumIn = mod.NewProc("VBVMR_Input_GetDeviceNumber") + //getDevDescIn = mod.NewProc("VBVMR_Input_GetDeviceDescA") + + vmMdirty = mod.NewProc("VBVMR_MacroButton_IsDirty") + getMacroButtonStatus = mod.NewProc("VBVMR_MacroButton_GetStatus") + setMacroButtonStatus = mod.NewProc("VBVMR_MacroButton_SetStatus") +) + +// login logs into the API, +// then attempts to launch Voicemeeter if it's not running. +func login(kind_id string) { + res, _, _ := vmLogin.Call() + if res == 1 { + runVoicemeeter(kind_id) + time.Sleep(time.Second) + } else if res != 0 { + err := fmt.Errorf("VBVMR_Login returned %d", res) + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Logged into API") + sync() +} + +// logout logs out of the API, +// delayed for 100ms to allow final operation to complete. +func logout() { + time.Sleep(100 * time.Millisecond) + res, _, _ := vmLogout.Call() + if res != 0 { + err := fmt.Errorf("VBVMR_Logout returned %d", res) + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Logged out of API") +} + +// runVoicemeeter attempts to launch a Voicemeeter GUI of a kind. +func runVoicemeeter(kind_id string) { + vals := map[string]uint64{ + "basic": 1, + "banana": 2, + "potato": 3, + } + res, _, _ := vmRunvm.Call(uintptr(vals[kind_id])) + if res != 0 { + err := fmt.Errorf("VBVMR_RunVoicemeeter returned %d", res) + fmt.Println(err) + os.Exit(1) + } +} + +// getVersion returns the version of Voicemeeter as a string +func getVersion() string { + var ver uint64 + res, _, _ := vmGetvmVersion.Call(uintptr(unsafe.Pointer(&ver))) + if res != 0 { + err := fmt.Errorf("VBVMR_GetVoicemeeterVersion returned %d", res) + fmt.Println(err) + os.Exit(1) + } + v1 := (ver & 0xFF000000) >> 24 + v2 := (ver & 0x00FF0000) >> 16 + v3 := (ver & 0x0000FF00) >> 8 + v4 := ver & 0x000000FF + return fmt.Sprintf("%d.%d.%d.%d", v1, v2, v3, v4) +} + +// pdirty returns true iff a parameter value has changed +func pdirty() bool { + res, _, _ := vmPdirty.Call() + return int(res) == 1 +} + +// mdirty returns true iff a macrobutton value has changed +func mdirty() bool { + res, _, _ := vmMdirty.Call() + return int(res) == 1 +} + +func sync() { + time.Sleep(5 * time.Millisecond) + for pdirty() || mdirty() { + } +} + +// getVMType returns the type of Voicemeeter, as a string +func getVMType() string { + var type_ uint64 + res, _, _ := vmGetvmType.Call( + uintptr(unsafe.Pointer(&type_)), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_GetVoicemeeterType returned %d", res) + fmt.Println(err) + os.Exit(1) + } + vals := map[uint64]string{ + 1: "basic", + 2: "banana", + 3: "potato", + } + return vals[type_] +} + +// getParameterFloat gets the value of a float parameter +func getParameterFloat(name string) float64 { + var value float32 + b := append([]byte(name), 0) + res, _, _ := getParamFloat.Call( + uintptr(unsafe.Pointer(&b[0])), + uintptr(unsafe.Pointer(&value)), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_GetParameterFloat returned %d", res) + fmt.Println(err) + os.Exit(1) + } + return math.Round(float64(value)*10) / 10 +} + +// getParameterFloat sets the value of a float parameter +func setParameterFloat(name string, value float32) { + b1 := append([]byte(name), 0) + b2 := math.Float32bits(value) + res, _, _ := setParamFloat.Call( + uintptr(unsafe.Pointer(&b1[0])), + uintptr(b2), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_SetParameterFloat returned %d", res) + fmt.Println(err) + os.Exit(1) + } + sync() +} + +// getParameterString gets the value of a string parameter +func getParameterString(name string) string { + b1 := append([]byte(name), 0) + var b2 [512]byte + res, _, _ := getParamString.Call( + uintptr(unsafe.Pointer(&b1[0])), + uintptr(unsafe.Pointer(&b2[0])), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_GetParameterStringA returned %d", res) + fmt.Println(err) + os.Exit(1) + } + str := bytes.Trim(b2[:], "\x00") + return string(str) +} + +// getParameterString sets the value of a string parameter +func setParameterString(name, value string) { + b1 := append([]byte(name), 0) + b2 := append([]byte(value), 0) + res, _, _ := setParamString.Call( + uintptr(unsafe.Pointer(&b1[0])), + uintptr(unsafe.Pointer(&b2[0])), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_SetParameterStringA returned %d", res) + fmt.Println(err) + os.Exit(1) + } + sync() +} + +// setParametersMulti sets multiple parameters with a script +func setParametersMulti(script string) { + b1 := append([]byte(script), 0) + res, _, _ := setParameters.Call( + uintptr(unsafe.Pointer(&b1[0])), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_SetParameters returned %d", res) + fmt.Println(err) + os.Exit(1) + } +} + +// getMacroStatus gets a macrobutton value +func getMacroStatus(id, mode int) float32 { + var state float32 + res, _, _ := getMacroButtonStatus.Call( + uintptr(id), + uintptr(unsafe.Pointer(&state)), + uintptr(mode), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_MacroButton_GetStatus returned %d", res) + fmt.Println(err) + os.Exit(1) + } + return state +} + +// setMacroStatus sets a macrobutton value +func setMacroStatus(id, state, mode int) { + res, _, _ := setMacroButtonStatus.Call( + uintptr(id), + uintptr(state), + uintptr(mode), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_MacroButton_SetStatus returned %d", res) + fmt.Println(err) + os.Exit(1) + } + time.Sleep(30 * time.Millisecond) + sync() +} + +/* +// getLevel returns a single level value of type type_ for channel[i] +func getLevel(type_, i int) float32 { + var val float32 + res, _, _ := getLevelFloat.Call( + uintptr(type_), + uintptr(i), + uintptr(unsafe.Pointer(&val)), + ) + if res != 0 { + err := fmt.Errorf("VBVMR_GetLevel returned %d", res) + fmt.Println(err) + os.Exit(1) + } + return val +} +*/ diff --git a/voicemeeter/bus.go b/voicemeeter/bus.go new file mode 100644 index 0000000..9cd977f --- /dev/null +++ b/voicemeeter/bus.go @@ -0,0 +1,44 @@ +package voicemeeter + +import ( + "fmt" +) + +// custom bus type, struct forwarding channel +type bus struct { + channel +} + +// newBus returns a strip type +// it also initializes embedded channel type +func newBus(i int, k *kind) bus { + return bus{channel{"bus", i, *k}} +} + +// String implements the stringer interface +func (b *bus) String() string { + if b.index < b.kind.physOut { + return fmt.Sprintf("PhysicalBus%d\n", b.index) + } + return fmt.Sprintf("VirtualBus%d\n", b.index) +} + +// GetMute returns the value of the Mute parameter +func (b *bus) GetMute() bool { + return b.getter_bool("Mute") +} + +// SetMute sets the value of the Mute parameter +func (b *bus) SetMute(val bool) { + b.setter_bool("Mute", val) +} + +// GetEq returns the value of the Eq.On parameter +func (b *bus) GetEq() bool { + return b.getter_bool("Eq.On") +} + +// SetEq sets the value of the Eq.On parameter +func (b *bus) SetEq(val bool) { + b.setter_bool("Eq.On", val) +} diff --git a/voicemeeter/button.go b/voicemeeter/button.go new file mode 100644 index 0000000..3e9dedf --- /dev/null +++ b/voicemeeter/button.go @@ -0,0 +1,64 @@ +package voicemeeter + +import "fmt" + +// custom strip type, struct forwarding channel +type button struct { + index int +} + +// getter returns the value of a macrobutton parameter +func (m *button) getter(mode int) bool { + return getMacroStatus(m.index, mode) == 1 +} + +// setter sets the value of a macrobutton parameter +func (m *button) setter(v bool, mode int) { + var value int + if v { + value = 1 + } else { + value = 0 + } + setMacroStatus(m.index, value, mode) +} + +// newButton returns a button type +func newButton(i int) button { + return button{i} +} + +// String implements the stringer interface +func (m *button) String() string { + return fmt.Sprintf("MacroButton%d\n", m.index) +} + +// GetState returns the value of the State parameter +func (m *button) GetState() bool { + return m.getter(1) +} + +// SetState sets the value of the State parameter +func (m *button) SetState(val bool) { + m.setter(val, 1) +} + +// GetStateOnly returns the value of the StateOnly parameter +func (m *button) GetStateOnly() bool { + return m.getter(2) +} + +// SetStateOnly sets the value of the StateOnly parameter +func (m *button) SetStateOnly(val bool) { + m.setter(val, 2) +} + +// GetTrigger returns the value of the Trigger parameter +func (m *button) GetTrigger() bool { + return m.getter(2) +} + +// SetTrigger returns the value of the Trigger parameter +func (m *button) SetTrigger(val bool) { + m.setter(val, 2) +} diff --git a/voicemeeter/cdll.go b/voicemeeter/cdll.go new file mode 100644 index 0000000..42d3e52 --- /dev/null +++ b/voicemeeter/cdll.go @@ -0,0 +1,56 @@ +package voicemeeter + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "golang.org/x/sys/windows/registry" +) + +// dllPath returns the Voicemeeter installation path as a string +func dllPath() (string, error) { + if runtime.GOOS != "windows" { + return "", errors.New("only Windows OS supported") + } + + var regkey string + if strings.Contains(runtime.GOARCH, "64") { + regkey = `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall` + } else { + regkey = `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` + } + var vmkey = `\VB:Voicemeeter {17359A74-1236-5467}` + + k, err := registry.OpenKey(registry.LOCAL_MACHINE, regkey+vmkey, registry.QUERY_VALUE) + if err != nil { + return "", errors.New("unable to access registry") + } + defer k.Close() + + path, _, err := k.GetStringValue(`UninstallString`) + if err != nil { + return "", errors.New("unable to read Voicemeeter path from registry") + } + + var dllName string + if strings.Contains(runtime.GOARCH, "64") { + dllName = `VoicemeeterRemote64.dll` + } else { + dllName = `VoicemeeterRemote.dll` + } + return fmt.Sprintf("%v\\%s", filepath.Dir(path), dllName), nil +} + +// getDllPath is a helper function for error handling +func getDllPath() string { + path, err := dllPath() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return path +} diff --git a/voicemeeter/channel.go b/voicemeeter/channel.go new file mode 100644 index 0000000..bee7d66 --- /dev/null +++ b/voicemeeter/channel.go @@ -0,0 +1,65 @@ +package voicemeeter + +import ( + "fmt" +) + +type channel struct { + identifier string + index int + kind kind +} + +// getter_bool returns the value of a boolean parameter +func (c *channel) getter_bool(p string) bool { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + return getParameterFloat(param) == 1 +} + +// setter_bool sets the value of a boolean parameter +func (c *channel) setter_bool(p string, v bool) { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + var value float32 + if v { + value = 1 + } else { + value = 0 + } + setParameterFloat(param, float32(value)) +} + +// getter_int returns the value of an int parameter p +func (c *channel) getter_int(p string) int { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + return int(getParameterFloat(param)) +} + +// setter_int sets the value v of an int parameter p +func (c *channel) setter_int(p string, v int) { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + setParameterFloat(param, float32(v)) +} + +// getter_float returns the value of an int parameter p +func (c *channel) getter_float(p string) float64 { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + return getParameterFloat(param) +} + +// setter_float sets the value v of an int parameter p +func (c *channel) setter_float(p string, v float32) { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + setParameterFloat(param, float32(v)) +} + +// getter_string returns the value of a string parameter p +func (c *channel) getter_string(p string) string { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + return getParameterString(param) +} + +// setter_string sets the value v of a string parameter p +func (c *channel) setter_string(p, v string) { + param := fmt.Sprintf("%s[%d].%s", c.identifier, c.index, p) + setParameterString(param, v) +} diff --git a/voicemeeter/kinds.go b/voicemeeter/kinds.go new file mode 100644 index 0000000..7cb4446 --- /dev/null +++ b/voicemeeter/kinds.go @@ -0,0 +1,44 @@ +package voicemeeter + +import ( + "fmt" + "strings" +) + +// A kind represents a Voicemeeter kinds layout +type kind struct { + name string + physIn int + virtIn int + physOut int + virtOut int +} + +func (k *kind) numStrip() int { + n := k.physIn + k.virtIn + return n +} + +func (k *kind) numBus() int { + n := k.physOut + k.virtOut + return n +} + +func (k *kind) String() string { + return fmt.Sprintf("%s%s", strings.ToUpper(k.name[:1]), k.name[1:]) +} + +// newBasicKind returns a basic kind struct address +func newBasicKind() *kind { + return &kind{"basic", 2, 1, 1, 1} +} + +// newBananaKind returns a banana kind struct address +func newBananaKind() *kind { + return &kind{"banana", 3, 2, 3, 2} +} + +// newPotatoKind returns a potato kind struct address +func newPotatoKind() *kind { + return &kind{"potato", 5, 3, 5, 3} +} diff --git a/voicemeeter/remote.go b/voicemeeter/remote.go new file mode 100644 index 0000000..375316e --- /dev/null +++ b/voicemeeter/remote.go @@ -0,0 +1,77 @@ +package voicemeeter + +import ( + "fmt" + "os" +) + +// A remote type represents the API for a kind, +// comprised of slices representing each member +type remote struct { + kind *kind + Strip []strip + Bus []bus + Button []button +} + +// String implements the stringer interface +func (r *remote) String() string { + return fmt.Sprintf("Voicemeeter %s", r.kind) +} + +func (r *remote) Login() { + login(r.kind.name) +} + +func (r *remote) Logout() { + logout() +} + +func (r *remote) Type() string { + return getVMType() +} + +func (r *remote) Version() string { + return getVersion() +} + +func (r *remote) SendText(script string) { + setParametersMulti(script) +} + +// NewRemote returns a remote type of a kind, +// this exported method is the interface entry point. +func NewRemote(kind_id string) remote { + kindMap := map[string]*kind{ + "basic": newBasicKind(), + "banana": newBananaKind(), + "potato": newPotatoKind(), + } + + _kind, ok := kindMap[kind_id] + if !ok { + err := fmt.Errorf("unknown Voicemeeter kind '%s'", kind_id) + fmt.Println(err) + os.Exit(1) + } + + _strip := make([]strip, _kind.numStrip()) + for i := 0; i < _kind.physIn+_kind.virtIn; i++ { + _strip[i] = newStrip(i, _kind) + } + _bus := make([]bus, _kind.numBus()) + for i := 0; i < _kind.physOut+_kind.virtOut; i++ { + _bus[i] = newBus(i, _kind) + } + _button := make([]button, 80) + for i := 0; i < 80; i++ { + _button[i] = newButton(i) + } + + return remote{ + kind: _kind, + Strip: _strip, + Bus: _bus, + Button: _button, + } +} diff --git a/voicemeeter/strip.go b/voicemeeter/strip.go new file mode 100644 index 0000000..e968a73 --- /dev/null +++ b/voicemeeter/strip.go @@ -0,0 +1,73 @@ +package voicemeeter + +import ( + "fmt" +) + +// custom strip type, struct forwarding channel +type strip struct { + channel +} + +// constructor method for strip +func newStrip(i int, k *kind) strip { + return strip{channel{"strip", i, *k}} +} + +// implement stringer interface in fmt +func (s *strip) String() string { + if s.index < s.kind.physIn { + return fmt.Sprintf("PhysicalStrip%d\n", s.index) + } + return fmt.Sprintf("VirtualStrip%d\n", s.index) +} + +// GetMute returns the value of the Mute parameter +func (s *strip) GetMute() bool { + return s.getter_bool("Mute") +} + +// SetMute sets the value of the Mute parameter +func (s *strip) SetMute(val bool) { + s.setter_bool("Mute", val) +} + +// GetLimit returns the value of the Limit parameter +func (s *strip) GetLimit() int { + return s.getter_int("Limit") +} + +// SetLimit sets the value of the Limit parameter +func (s *strip) SetLimit(val int) { + s.setter_int("Limit", val) +} + +// GetMc returns the value of the MC parameter +func (s *strip) GetMc() bool { + return s.getter_bool("MC") +} + +// SetMc sets the value of the MC parameter +func (s *strip) SetMc(val bool) { + s.setter_bool("MC", val) +} + +// GetMc returns the value of the MC parameter +func (s *strip) GetLabel() string { + return s.getter_string("Label") +} + +// SetMc sets the value of the MC parameter +func (s *strip) SetLabel(val string) { + s.setter_string("Label", val) +} + +// GetGain returns the value of the Gain parameter +func (s *strip) GetGain() float64 { + return s.getter_float("Gain") +} + +// SetGain sets the value of the Gain parameter +func (s *strip) SetGain(val float32) { + s.setter_float("Gain", val) +}