mirror of
https://github.com/onyx-and-iris/vbantxt.git
synced 2026-04-07 17:53:30 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92d5d96967 | |||
| 41be07666c | |||
| 0852b61eea | |||
| 6c53cfa383 | |||
| ee781ea586 | |||
| a6b20bf676 | |||
| d0bd5ca31a | |||
| 74a55dadad | |||
| dde8473c31 | |||
| 60ad386431 | |||
| c73614133d | |||
| 2bdabdae03 | |||
| 31b188280d | |||
| ccf34e492d | |||
|
|
56ce415d6d | ||
|
|
cfd89fb1ed | ||
|
|
73a99e5059 | ||
|
|
6221f6a167 | ||
|
|
826df820fc |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
bin/
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
@@ -13,5 +14,3 @@
|
|||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
*.txt
|
|
||||||
43
CHANGELOG.md
Normal file
43
CHANGELOG.md
Normal 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.
|
||||||
97
README.md
97
README.md
@@ -1,38 +1,88 @@
|
|||||||
# vbantxt
|

|
||||||
|

|
||||||
|
|
||||||
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
|
## Tested against
|
||||||
|
|
||||||
- Basic 1.0.8.4
|
- Basic 1.0.8.4
|
||||||
- Banana 2.0.6.4
|
- Banana 2.0.6.4
|
||||||
- Potato 3.0.2.4
|
- Potato 3.0.2.4
|
||||||
|
- Matrix 1.0.0.3
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Voicemeeter](https://voicemeeter.com/)
|
- [Voicemeeter](https://voicemeeter.com/) or [Matrix](https://vb-audio.com/Matrix/)
|
||||||
- Go 1.18 or greater
|
- Go 1.18 or greater (if you want to compile yourself, otherwise check `Releases`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## `Use`
|
## `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:
|
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:
|
A valid `config.toml` might look like this:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[connection]
|
[connection]
|
||||||
Host="gamepc.local"
|
Host="gamepc.local"
|
||||||
Port=6990
|
Port=6980
|
||||||
Streamname="Command1"
|
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.
|
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)`
|
`vbantxt-cli $(Get-Content .\script.txt)`
|
||||||
|
|
||||||
|
Or with Bash:
|
||||||
|
|
||||||
|
`cat script.txt | xargs vbantxt-cli`
|
||||||
|
|
||||||
to load commands from a file:
|
to load commands from a file:
|
||||||
|
|
||||||
```
|
```
|
||||||
strip[0].mute=0;strip[0].mute=0
|
strip[0].mute=1;strip[0].mono=0
|
||||||
strip[1].mono=0;strip[1].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
48
client.go
Normal 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
42
cmd/vbantxt/config.go
Normal 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
|
||||||
|
}
|
||||||
39
cmd/vbantxt/config_internal_test.go
Normal file
39
cmd/vbantxt/config_internal_test.go
Normal 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
97
cmd/vbantxt/main.go
Normal 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
4
cmd/vbantxt/testdata/config.toml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[connection]
|
||||||
|
host = "localhost"
|
||||||
|
port = 7000
|
||||||
|
streamname = "vbantxt"
|
||||||
26
cmd/vbantxt/util.go
Normal file
26
cmd/vbantxt/util.go
Normal 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
9
errors.go
Normal 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
10
go.mod
@@ -1,10 +1,16 @@
|
|||||||
module github.com/onyx-and-iris/vbantxt-cli
|
module github.com/onyx-and-iris/vbantxt
|
||||||
|
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.2.1
|
github.com/BurntSushi/toml v1.2.1
|
||||||
github.com/sirupsen/logrus v1.9.0
|
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
7
go.sum
@@ -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 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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/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.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 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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/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.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
155
main.go
@@ -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
40
makefile
Normal 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
35
option.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
70
packet.go
70
packet.go
@@ -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
|
const (
|
||||||
type requestHeader struct {
|
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
|
name string
|
||||||
bpsIndex int
|
bpsIndex int
|
||||||
channel int
|
channel int
|
||||||
framecounter []byte
|
framecounter []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRequestHeader returns a pointer to a requestHeader struct as a singleton
|
// newPacket returns a packet struct with default values, framecounter at 0.
|
||||||
func newRequestHeader(streamname string, bpsI, channel int) *requestHeader {
|
func newPacket(streamname string) packet {
|
||||||
if r != nil {
|
return packet{
|
||||||
return r
|
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
|
// sr defines the samplerate for the request
|
||||||
func (r *requestHeader) sr() byte {
|
func (p *packet) sr() byte {
|
||||||
return byte(VBAN_PROTOCOL_TXT + r.bpsIndex)
|
return byte(vbanProtocolTxt + p.bpsIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nbc defines the channel of the request
|
// nbc defines the channel of the request
|
||||||
func (r *requestHeader) nbc() byte {
|
func (p *packet) nbc() byte {
|
||||||
return byte(r.channel)
|
return byte(p.channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// streamname defines the stream name of the text request
|
// streamname defines the stream name of the text request
|
||||||
func (r *requestHeader) streamname() []byte {
|
func (p *packet) streamname() []byte {
|
||||||
b := make([]byte, 16)
|
b := make([]byte, streamNameSz)
|
||||||
copy(b, r.name)
|
copy(b, p.name)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// header returns a fully formed text request packet header
|
// header returns a fully formed packet header
|
||||||
func (t *requestHeader) header() []byte {
|
func (p *packet) header() []byte {
|
||||||
h := []byte("VBAN")
|
h := make([]byte, 0, headerSz)
|
||||||
h = append(h, t.sr())
|
h = append(h, []byte("VBAN")...)
|
||||||
|
h = append(h, p.sr())
|
||||||
h = append(h, byte(0))
|
h = append(h, byte(0))
|
||||||
h = append(h, t.nbc())
|
h = append(h, p.nbc())
|
||||||
h = append(h, byte(0x10))
|
h = append(h, byte(0x10))
|
||||||
h = append(h, t.streamname()...)
|
h = append(h, p.streamname()...)
|
||||||
h = append(h, t.framecounter...)
|
h = append(h, p.framecounter...)
|
||||||
return h
|
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
4
testdata/matrix.txt
vendored
Normal 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
3
testdata/vm.txt
vendored
Normal 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
11
util.go
@@ -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
59
vbantxt.go
Normal 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
42
vbantxt_test.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user