10 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
20 changed files with 562 additions and 203 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

View File

@@ -9,23 +9,35 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
## [Unreleased]
- [x]
- [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.
- 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.
- `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.
- 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,26 +1,60 @@
![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
# VBAN Sendtext
Send Voicemeeter string requests over a network or to Matrix
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
- 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/) or [Matrix](https://vb-audio.com/Matrix/)
- Go 1.18 or greater (if you want to compile yourself, otherwise check `Releases`)
- [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`
`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:
@@ -29,7 +63,7 @@ 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"
```
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:
@@ -40,9 +74,9 @@ Port=6980
Streamname="Command1"
```
- `host` defaults to "localhost"
- `port` defaults to 6980
- `streamname` defaults to "Command1"
- `host` defaults to "localhost"
- `port` defaults to 6980
- `streamname` defaults to "Command1"
Command line flags will override values in a config.toml.

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"

View File

@@ -1,8 +1,21 @@
package main
import "flag"
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
}
// 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 {
@@ -11,13 +24,3 @@ func indexOf[T comparable](collection []T, e T) int {
}
return -1
}
func isFlagPassed(name string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}

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=

142
main.go
View File

@@ -1,142 +0,0 @@
package main
import (
"bytes"
"encoding/binary"
"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 main() {
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(&delay, "delay", 20, "delay between requests")
flag.IntVar(&delay, "d", 20, "delay between requests (shorthand)")
flag.IntVar(&loglevel, "loglevel", int(log.WarnLevel), "log level")
flag.IntVar(&loglevel, "l", int(log.WarnLevel), "log level (shorthand)")
flag.Parse()
if loglevel >= int(log.PanicLevel) && loglevel <= int(log.TraceLevel) {
log.SetLevel(log.Level(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.Error())
}
}
}
// vbanConnect establishes a VBAN connection to remote host
func vbanConnect() (*net.UDPConn, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
f := filepath.Join(homeDir, ".vbantxt_cli", "config.toml")
if _, err := os.Stat(f); err == nil {
conn, err := connFromToml(f)
if err != nil {
return nil, err
}
if !isFlagPassed("h") && !isFlagPassed("host") {
host = conn.Host
}
if !isFlagPassed("p") && !isFlagPassed("port") {
port = conn.Port
}
if !isFlagPassed("s") && !isFlagPassed("streamname") {
streamname = conn.Streamname
}
}
log.Debugf("Using values host: %s port: %d streamname: %s", host, port, streamname)
s, _ := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", host, port))
c, err := net.DialUDP("udp4", nil, s)
if err != nil {
return nil, err
}
log.Infof("Connected to %s", c.RemoteAddr())
return c, nil
}
// connFromToml parses connection info from config.toml
func connFromToml(f string) (*connection, error) {
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.Debugf("Sending '%s' to: %s", msg, c.RemoteAddr())
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

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