19 Commits

Author SHA1 Message Date
92d5d96967 add makefile 2024-11-03 15:47:59 +00:00
41be07666c section 0.2.0 added to CHANGELOG
README updated, use section added.

default config.toml dir updated
2024-11-03 15:47:54 +00:00
0852b61eea add external vm/matrix tests 2024-11-03 15:47:03 +00:00
6c53cfa383 reorganise some of the internals of the package.
functional options added.
2024-11-03 15:46:14 +00:00
ee781ea586 separate the cli from the vbantxt package
add internal LoadConfig test
2024-11-03 15:44:10 +00:00
a6b20bf676 remove old cli files 2024-11-03 15:42:39 +00:00
d0bd5ca31a upd tested against 2024-06-28 22:23:47 +01:00
74a55dadad add matrix script file example 2024-06-28 20:18:56 +01:00
dde8473c31 reorder README 2024-06-28 19:53:02 +01:00
60ad386431 add link to matrix commands to README 2024-06-28 09:12:42 +01:00
c73614133d add link to CHANGELOG in README 2024-06-28 06:56:45 +01:00
2bdabdae03 host now defaults to "localhost"
loglevel flag now uses logrus constants (0 up to 6)

config values are only applied if the corresponding flag wasn't passed
isFlagPassed() added to util.go
2024-06-28 06:55:53 +01:00
31b188280d CHANGELOG added 2024-06-28 06:51:23 +01:00
ccf34e492d Added link to Matrix.
Added some details regarding command line flags.

Matrix, Logging sections added
2024-06-28 06:51:10 +01:00
onyx-and-iris
56ce415d6d typo fix 2022-11-06 07:47:25 +00:00
onyx-and-iris
cfd89fb1ed update readme 2022-11-06 07:37:30 +00:00
onyx-and-iris
73a99e5059 alter port in readme toml example 2022-11-05 19:33:26 +00:00
onyx-and-iris
6221f6a167 add os badges 2022-11-05 19:27:16 +00:00
onyx-and-iris
826df820fc adjust readme header 2022-11-05 19:05:38 +00:00
21 changed files with 637 additions and 207 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
bin/
# Test binary, built with `go test -c`
*.test
@@ -13,5 +14,3 @@
# Dependency directories (remove the comment below to include it)
# vendor/
*.txt

43
CHANGELOG.md Normal file
View File

@@ -0,0 +1,43 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Before any major/minor/patch bump all unit tests will be run to verify they pass.
## [Unreleased]
- [x]
# [0.2.0] - 2024-10-27
### Added
- `config` flag (shorthand `C`), you may now specify a custom config directory. It defaults to `home directory / .config / vbantxt_cli /`.
- please note, the default directory has changed from v0.1.0
### Changed
- Behaviour change: if any one of `"host", "h", "port", "p", "streamname", "s"` flags are passed then the config file will be ignored.
- `delay` flag changed to `ratelimit` (shorthand `r`). It defaults to 20ms.
# [0.1.0] - 2024-06-28
### Added
- Matrix and Logging sections to README.
### Changed
- `host` flag now defaults to "localhost". Useful if sending VBAN-Text to Matrix
- `loglevel` flag now expects values that correspond to the logrus package loglevels (0 up to 6). See README.
- Config values are only applied if the corresponding flag was not passed on the command line.
# [0.0.1] - 2022-09-23
### Added
- Initial release, package implements VBAN PROTOCOL TXT with a basic CLI for configuring options.
- Ability to load configuration settings from a config.toml.

View File

@@ -1,38 +1,88 @@
# vbantxt
![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white)
![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)
VBAN sendtext cli utility for sending Voicemeeter string requests over a network.
# VBAN Sendtext
Send Voicemeeter/Matrix vban requests.
For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Tested against
- Basic 1.0.8.4
- Banana 2.0.6.4
- Potato 3.0.2.4
- Matrix 1.0.0.3
## Requirements
- [Voicemeeter](https://voicemeeter.com/)
- Go 1.18 or greater
- [Voicemeeter](https://voicemeeter.com/) or [Matrix](https://vb-audio.com/Matrix/)
- Go 1.18 or greater (if you want to compile yourself, otherwise check `Releases`)
---
## `Use`
#### Command Line
`go get github.com/onyx-and-iris/vbantxt`
```go
package main
import (
"log"
"github.com/onyx-and-iris/vbantxt"
)
func main() {
var (
host string = "vm.local"
port int = 6980
streamname string = "onyx"
)
vbantxtClient, err := vbantxt.New(host, port, streamname)
if err != nil {
log.Fatal(err)
}
defer vbantxtClient.Close()
err = vbantxtClient.Send("strip[0].mute=0")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Error: %s", err)
os.Exit(1)
}
}
```
## `Command Line`
Pass `host`, `port` and `streamname` as flags, for example:
`vbantxt-cli -h="gamepc.local" -p=6980 -s=Command1 "strip[0].mute=1 strip[1].mono=1"`
```
vbantxt-cli -h="gamepc.local" -p=6980 -s=Command1 "strip[0].mute=1 strip[1].mono=1"
```
You may also store them in a `config.toml` located in `home directory / .vbantxt_cli /`
You may also store them in a `config.toml` located in `home directory / .config / vbantxt_cli /`
A valid `config.toml` might look like this:
```toml
[connection]
Host="gamepc.local"
Port=6990
Port=6980
Streamname="Command1"
```
#### Script files
- `host` defaults to "localhost"
- `port` defaults to 6980
- `streamname` defaults to "Command1"
Command line flags will override values in a config.toml.
---
## `Script files`
The vbantxt-cli utility accepts a single string request or an array of string requests. This means you can pass scripts stored in files.
@@ -40,9 +90,34 @@ For example, in Windows with Powershell you could:
`vbantxt-cli $(Get-Content .\script.txt)`
Or with Bash:
`cat script.txt | xargs vbantxt-cli`
to load commands from a file:
```
strip[0].mute=0;strip[0].mute=0
strip[1].mono=0;strip[1].mono=0
strip[0].mute=1;strip[0].mono=0
strip[1].mute=0;strip[1].mono=1
bus[3].eq.On=0
```
---
## `Matrix`
Sending commands to VB-Audio Matrix is also possible, for example:
```
vbantxt-cli -s=streamname "Point(ASIO128.IN[2],ASIO128.OUT[1]).dBGain = -8"
```
---
## `Logging`
Log level may be set by passing the `-l` flag with a number from 0 up to 6 where
0 = Panic, 1 = Fatal, 2 = Error, 3 = Warning, 4 = Info, 5 = Debug, 6 = Trace
Log level defaults to Warning level.

48
client.go Normal file
View File

@@ -0,0 +1,48 @@
package vbantxt
import (
"fmt"
"net"
log "github.com/sirupsen/logrus"
)
// client represents the UDP client
type client struct {
conn *net.UDPConn
}
// NewClient returns a UDP client
func newClient(host string, port int) (client, error) {
udpAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return client{}, err
}
conn, err := net.DialUDP("udp4", nil, udpAddr)
if err != nil {
return client{}, err
}
log.Infof("Outgoing address %s", conn.RemoteAddr())
return client{conn: conn}, nil
}
// Write implements the io.WriteCloser interface
func (c client) Write(buf []byte) (int, error) {
n, err := c.conn.Write(buf)
if err != nil {
return 0, err
}
log.Debugf("Sending '%s' to: %s", string(buf), c.conn.RemoteAddr())
return n, nil
}
// Close implements the io.WriteCloser interface
func (c client) Close() error {
err := c.conn.Close()
if err != nil {
return err
}
return nil
}

42
cmd/vbantxt/config.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"fmt"
"os"
"github.com/BurntSushi/toml"
log "github.com/sirupsen/logrus"
)
type config struct {
Connection connection `toml:"connection"`
}
func (c config) String() string {
return fmt.Sprintf(
"host: %s port: %d streamname: %s",
c.Connection.Host, c.Connection.Port, c.Connection.Streamname)
}
type connection struct {
Host string `toml:"host"`
Port int `toml:"port"`
Streamname string `toml:"streamname"`
}
func loadConfig(configPath string) (*connection, error) {
_, err := os.Stat(configPath)
if err != nil {
return nil, err
}
var config config
_, err = toml.DecodeFile(configPath, &config)
if err != nil {
return nil, err
}
log.Debug(config)
return &config.Connection, nil
}

View File

@@ -0,0 +1,39 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadConfig_Success(t *testing.T) {
conn, err := loadConfig("testdata/config.toml")
require.NoError(t, err)
assert.Equal(t, conn.Host, "localhost")
assert.Equal(t, conn.Port, 7000)
assert.Equal(t, conn.Streamname, "vbantxt")
}
func TestLoadConfig_Errors(t *testing.T) {
tt := map[string]struct {
input string
err string
}{
"no such file": {
input: "/no/such/dir/config.toml",
err: "no such file or directory",
},
}
for name, tc := range tt {
_, err := loadConfig("/no/such/dir/config.toml")
t.Run(name, func(t *testing.T) {
assert.Error(t, err)
assert.ErrorContains(t, err, tc.err)
})
}
}

97
cmd/vbantxt/main.go Normal file
View File

@@ -0,0 +1,97 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"slices"
"time"
"github.com/onyx-and-iris/vbantxt"
log "github.com/sirupsen/logrus"
)
func exit(err error) {
_, _ = fmt.Fprintf(os.Stderr, "Error: %s", err)
os.Exit(1)
}
func main() {
var (
host string
port int
streamname string
loglevel int
configPath string
bps int
channel int
ratelimit int
)
flag.StringVar(&host, "host", "localhost", "vban host")
flag.StringVar(&host, "h", "localhost", "vban host (shorthand)")
flag.IntVar(&port, "port", 6980, "vban server port")
flag.IntVar(&port, "p", 6980, "vban server port (shorthand)")
flag.StringVar(&streamname, "streamname", "Command1", "stream name for text requests")
flag.StringVar(&streamname, "s", "Command1", "stream name for text requests (shorthand)")
flag.IntVar(&bps, "bps", 0, "vban bps")
flag.IntVar(&bps, "b", 0, "vban bps (shorthand)")
flag.IntVar(&channel, "channel", 0, "vban channel")
flag.IntVar(&channel, "c", 0, "vban channel (shorthand)")
flag.IntVar(&ratelimit, "ratelimit", 20, "request ratelimit in milliseconds")
flag.IntVar(&ratelimit, "r", 20, "request ratelimit in milliseconds (shorthand)")
homeDir, err := os.UserHomeDir()
if err != nil {
exit(err)
}
defaultConfigPath := filepath.Join(homeDir, ".config", "vbantxt", "config.toml")
flag.StringVar(&configPath, "config", defaultConfigPath, "config path")
flag.StringVar(&configPath, "C", defaultConfigPath, "config path (shorthand)")
flag.IntVar(&loglevel, "loglevel", int(log.WarnLevel), "log level")
flag.IntVar(&loglevel, "l", int(log.WarnLevel), "log level (shorthand)")
flag.Parse()
if slices.Contains(log.AllLevels, log.Level(loglevel)) {
log.SetLevel(log.Level(loglevel))
}
if !flagsPassed([]string{"host", "h", "port", "p", "streamname", "s"}) {
config, err := loadConfig(configPath)
if err != nil {
exit(err)
}
host = config.Host
port = config.Port
streamname = config.Streamname
}
client, err := createClient(host, port, streamname, bps, channel, ratelimit)
if err != nil {
exit(err)
}
defer client.Close()
for _, arg := range flag.Args() {
err := client.Send(arg)
if err != nil {
log.Error(err)
}
}
}
func createClient(host string, port int, streamname string, bps int, channel, ratelimit int) (*vbantxt.VbanTxt, error) {
client, err := vbantxt.New(
host,
port,
streamname,
vbantxt.WithBPSOpt(indexOf(vbantxt.BpsOpts, bps)),
vbantxt.WithChannel(channel),
vbantxt.WithRateLimit(time.Duration(ratelimit)*time.Millisecond))
if err != nil {
return nil, err
}
return client, err
}

4
cmd/vbantxt/testdata/config.toml vendored Normal file
View File

@@ -0,0 +1,4 @@
[connection]
host = "localhost"
port = 7000
streamname = "vbantxt"

26
cmd/vbantxt/util.go Normal file
View File

@@ -0,0 +1,26 @@
package main
import (
"flag"
"slices"
)
func flagsPassed(flags []string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if slices.Contains(flags, f.Name) {
found = true
return
}
})
return found
}
func indexOf[T comparable](collection []T, e T) int {
for i, x := range collection {
if x == e {
return i
}
}
return -1
}

9
errors.go Normal file
View File

@@ -0,0 +1,9 @@
package vbantxt
// Error is used to define sentinel errors.
type Error string
// Error implements the error interface.
func (r Error) Error() string {
return string(r)
}

10
go.mod
View File

@@ -1,10 +1,16 @@
module github.com/onyx-and-iris/vbantxt-cli
module github.com/onyx-and-iris/vbantxt
go 1.19
require (
github.com/BurntSushi/toml v1.2.1
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.9.0
)
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

7
go.sum
View File

@@ -8,10 +8,13 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

155
main.go
View File

@@ -1,155 +0,0 @@
package main
import (
"bytes"
"encoding/binary"
"errors"
"flag"
"fmt"
"net"
"os"
"path/filepath"
"time"
"github.com/BurntSushi/toml"
log "github.com/sirupsen/logrus"
)
var (
host string
port int
streamname string
bps int
channel int
delay int
loglevel int
bpsOpts = []int{0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250,
38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600,
1000000, 1500000, 2000000, 3000000}
)
type (
// connection represents the configurable fields of a config.toml
connection struct {
Host string
Port int
Streamname string
}
// config maps toml headers
config struct {
Connection map[string]connection
}
)
func setLogLevel(loglevel int) {
switch loglevel {
case 0:
log.SetLevel(log.WarnLevel)
case 1:
log.SetLevel(log.InfoLevel)
case 2:
log.SetLevel(log.DebugLevel)
}
}
func main() {
flag.StringVar(&host, "host", "", "vban host")
flag.StringVar(&host, "h", "", "vban host (shorthand)")
flag.IntVar(&port, "port", 6980, "vban server port")
flag.IntVar(&port, "p", 6980, "vban server port (shorthand)")
flag.StringVar(&streamname, "streamname", "Command1", "stream name for text requests")
flag.StringVar(&streamname, "s", "Command1", "stream name for text requests (shorthand)")
flag.IntVar(&bps, "bps", 0, "vban bps")
flag.IntVar(&bps, "b", 0, "vban bps (shorthand)")
flag.IntVar(&channel, "channel", 0, "vban channel")
flag.IntVar(&channel, "c", 0, "vban channel (shorthand)")
flag.IntVar(&delay, "delay", 20, "delay between requests")
flag.IntVar(&delay, "d", 20, "delay between requests (shorthand)")
flag.IntVar(&loglevel, "loglevel", 0, "log level")
flag.IntVar(&loglevel, "l", 0, "log level (shorthand)")
flag.Parse()
setLogLevel(loglevel)
c, err := vbanConnect()
if err != nil {
log.Fatal(err)
}
defer c.Close()
header := newRequestHeader(streamname, indexOf(bpsOpts, bps), channel)
for _, arg := range flag.Args() {
err := send(c, header, arg)
if err != nil {
log.Error(err)
}
}
}
// vbanConnect establishes a VBAN connection to remote host
func vbanConnect() (*net.UDPConn, error) {
if host == "" {
conn, err := connFromToml()
if err != nil {
return nil, err
}
host = conn.Host
port = conn.Port
streamname = conn.Streamname
if host == "" {
err := errors.New("must provide a host with --host flag or config.toml")
return nil, err
}
}
CONNECT := fmt.Sprintf("%s:%d", host, port)
s, _ := net.ResolveUDPAddr("udp4", CONNECT)
c, err := net.DialUDP("udp4", nil, s)
if err != nil {
return nil, err
}
log.Info("Connected to ", c.RemoteAddr().String())
return c, nil
}
// connFromToml parses connection info from config.toml
func connFromToml() (*connection, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}
f := filepath.Join(homeDir, ".vbantxt_cli", "config.toml")
if _, err := os.Stat(f); err != nil {
err := fmt.Errorf("unable to locate %s", f)
return nil, err
}
var c config
_, err = toml.DecodeFile(f, &c.Connection)
if err != nil {
return nil, err
}
conn := c.Connection["connection"]
return &conn, nil
}
// send sends a VBAN text request over UDP to remote host
func send(c *net.UDPConn, h *requestHeader, msg string) error {
log.Debug("Sending '", msg, "' to: ", c.RemoteAddr().String())
data := []byte(msg)
_, err := c.Write(append(h.header(), data...))
if err != nil {
return err
}
var a uint32
_ = binary.Read(bytes.NewReader(h.framecounter[:]), binary.LittleEndian, &a)
binary.LittleEndian.PutUint32(h.framecounter[:], a+1)
time.Sleep(time.Duration(delay) * time.Millisecond)
return nil
}

40
makefile Normal file
View File

@@ -0,0 +1,40 @@
program = vbantxt
GO = go
BIN_DIR := bin
WINDOWS=$(BIN_DIR)/$(program)_windows_amd64.exe
LINUX=$(BIN_DIR)/$(program)_linux_amd64
VERSION=$(shell git describe --tags --always --long --dirty)
.DEFAULT_GOAL := build
.PHONY: fmt vet build windows linux test clean
fmt:
$(GO) fmt ./...
vet: fmt
$(GO) vet ./...
build: vet windows linux | $(BIN_DIR)
@echo version: $(VERSION)
windows: $(WINDOWS)
linux: $(LINUX)
$(WINDOWS):
env GOOS=windows GOARCH=amd64 go build -v -o $(WINDOWS) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/vbantxt/
$(LINUX):
env GOOS=linux GOARCH=amd64 go build -v -o $(LINUX) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/vbantxt/
test:
$(GO) test ./...
$(BIN_DIR):
@mkdir -p $@
clean:
@rm -rv $(BIN_DIR)

35
option.go Normal file
View File

@@ -0,0 +1,35 @@
package vbantxt
import (
"time"
log "github.com/sirupsen/logrus"
)
// Option is a functional option type that allows us to configure the VbanTxt.
type Option func(*VbanTxt)
// WithRateLimit is a functional option to set the ratelimit for requests
func WithRateLimit(ratelimit time.Duration) Option {
return func(vt *VbanTxt) {
vt.ratelimit = ratelimit
}
}
// WithBPSOpt is a functional option to set the bps index for {VbanTx}.{Packet}.bpsIndex
func WithBPSOpt(bpsIndex int) Option {
return func(vt *VbanTxt) {
if bpsIndex < 0 || bpsIndex >= len(BpsOpts) {
log.Warnf("invalid bpsIndex %d, defaulting to 0", bpsIndex)
return
}
vt.packet.bpsIndex = bpsIndex
}
}
// WithChannel is a functional option to set the bps index for {VbanTx}.{Packet}.channel
func WithChannel(channel int) Option {
return func(vt *VbanTxt) {
vt.packet.channel = channel
}
}

View File

@@ -1,50 +1,72 @@
package main
package vbantxt
var r *requestHeader
import (
"encoding/binary"
const VBAN_PROTOCOL_TXT = 0x40
log "github.com/sirupsen/logrus"
)
// requestHeader represents a single request header
type requestHeader struct {
const (
vbanProtocolTxt = 0x40
streamNameSz = 16
headerSz = 4 + 1 + 1 + 1 + 1 + 16 + 4
)
var BpsOpts = []int{0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250,
38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600,
1000000, 1500000, 2000000, 3000000}
type packet struct {
name string
bpsIndex int
channel int
framecounter []byte
}
// newRequestHeader returns a pointer to a requestHeader struct as a singleton
func newRequestHeader(streamname string, bpsI, channel int) *requestHeader {
if r != nil {
return r
// newPacket returns a packet struct with default values, framecounter at 0.
func newPacket(streamname string) packet {
return packet{
name: streamname,
bpsIndex: 0,
channel: 0,
framecounter: make([]byte, 4),
}
return &requestHeader{streamname, bpsI, channel, make([]byte, 4)}
}
// sr defines the samplerate for the request
func (r *requestHeader) sr() byte {
return byte(VBAN_PROTOCOL_TXT + r.bpsIndex)
func (p *packet) sr() byte {
return byte(vbanProtocolTxt + p.bpsIndex)
}
// nbc defines the channel of the request
func (r *requestHeader) nbc() byte {
return byte(r.channel)
func (p *packet) nbc() byte {
return byte(p.channel)
}
// streamname defines the stream name of the text request
func (r *requestHeader) streamname() []byte {
b := make([]byte, 16)
copy(b, r.name)
func (p *packet) streamname() []byte {
b := make([]byte, streamNameSz)
copy(b, p.name)
return b
}
// header returns a fully formed text request packet header
func (t *requestHeader) header() []byte {
h := []byte("VBAN")
h = append(h, t.sr())
// header returns a fully formed packet header
func (p *packet) header() []byte {
h := make([]byte, 0, headerSz)
h = append(h, []byte("VBAN")...)
h = append(h, p.sr())
h = append(h, byte(0))
h = append(h, t.nbc())
h = append(h, p.nbc())
h = append(h, byte(0x10))
h = append(h, t.streamname()...)
h = append(h, t.framecounter...)
h = append(h, p.streamname()...)
h = append(h, p.framecounter...)
return h
}
// bumpFrameCounter increments the frame counter by 1
func (p *packet) bumpFrameCounter() {
x := binary.LittleEndian.Uint32(p.framecounter)
binary.LittleEndian.PutUint32(p.framecounter, x+1)
log.Tracef("framecounter: %d", x)
}

4
testdata/matrix.txt vendored Normal file
View File

@@ -0,0 +1,4 @@
Point(ASIO128.IN[1..4],ASIO128.OUT[1]).dBGain = -3
Point(ASIO128.IN[1..4],ASIO128.OUT[2]).dBGain = -3
Point(ASIO128.IN[1..4],ASIO128.OUT[1]).Mute = 1
Point(ASIO128.IN[1..4],ASIO128.OUT[2]).Mute = 1

3
testdata/vm.txt vendored Normal file
View File

@@ -0,0 +1,3 @@
strip[0].mute=1;strip[0].mono=0
strip[1].mute=0;strip[1].mono=1
bus[3].eq.On=0

11
util.go
View File

@@ -1,11 +0,0 @@
package main
// indexOf returns the index of an element in an array
func indexOf[T comparable](collection []T, e T) int {
for i, x := range collection {
if x == e {
return i
}
}
return -1
}

59
vbantxt.go Normal file
View File

@@ -0,0 +1,59 @@
package vbantxt
import (
"fmt"
"io"
"time"
)
// VbanTxt is used to send VBAN-TXT requests to a distant Voicemeeter/Matrix.
type VbanTxt struct {
client io.WriteCloser
packet packet
ratelimit time.Duration
}
// New constructs a fully formed VbanTxt instance. This is the package's entry point.
// It sets default values for it's fields and then runs the option functions.
func New(host string, port int, streamname string, options ...Option) (*VbanTxt, error) {
client, err := newClient(host, port)
if err != nil {
return nil, fmt.Errorf("error creating UDP client for (%s:%d): %w", host, port, err)
}
vt := &VbanTxt{
client: client,
packet: newPacket(streamname),
ratelimit: time.Duration(20) * time.Millisecond,
}
for _, o := range options {
o(vt)
}
return vt, nil
}
// Send is resonsible for firing each VBAN-TXT request.
// It waits for {vt.ratelimit} time before returning.
func (vt VbanTxt) Send(cmd string) error {
_, err := vt.client.Write(append(vt.packet.header(), []byte(cmd)...))
if err != nil {
return fmt.Errorf("error sending command (%s): %w", cmd, err)
}
vt.packet.bumpFrameCounter()
time.Sleep(vt.ratelimit)
return nil
}
// Close is responsible for closing the UDP Client connection
func (vt VbanTxt) Close() error {
err := vt.client.Close()
if err != nil {
return fmt.Errorf("error attempting to close UDP Client: %w", err)
}
return nil
}

42
vbantxt_test.go Normal file
View File

@@ -0,0 +1,42 @@
package vbantxt_test
import (
"bufio"
"bytes"
_ "embed"
"testing"
"github.com/onyx-and-iris/vbantxt"
"github.com/stretchr/testify/require"
)
//go:embed testdata/vm.txt
var vm []byte
//go:embed testdata/matrix.txt
var matrix []byte
func run(t *testing.T, client *vbantxt.VbanTxt, script []byte) {
t.Helper()
r := bytes.NewReader(script)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
err := client.Send(scanner.Text())
require.NoError(t, err)
}
}
func TestSendVm(t *testing.T) {
client, err := vbantxt.New("vm.local", 6980, "onyx")
require.NoError(t, err)
run(t, client, vm)
}
func TestSendMatrix(t *testing.T) {
client, err := vbantxt.New("vm.local", 6990, "onyx")
require.NoError(t, err)
run(t, client, matrix)
}