initial commit

interface code + a few tests
This commit is contained in:
onyx-and-iris 2022-06-22 20:51:25 +01:00
parent 2dacfab629
commit 9d16f4c534
12 changed files with 759 additions and 2 deletions

View File

@ -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

5
go.mod Normal file
View File

@ -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

2
go.sum Normal file
View File

@ -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=

51
tests/higher_test.go Normal file
View File

@ -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")
}
}

264
voicemeeter/base.go Normal file
View File

@ -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
}
*/

44
voicemeeter/bus.go Normal file
View File

@ -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)
}

64
voicemeeter/button.go Normal file
View File

@ -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)
}

56
voicemeeter/cdll.go Normal file
View File

@ -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
}

65
voicemeeter/channel.go Normal file
View File

@ -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)
}

44
voicemeeter/kinds.go Normal file
View File

@ -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}
}

77
voicemeeter/remote.go Normal file
View File

@ -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,
}
}

73
voicemeeter/strip.go Normal file
View File

@ -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)
}