98 Commits

Author SHA1 Message Date
5d9924ff58 replace exitOnError with a deferred exit function.
this ensures the closer() cleanup function is always called.
2026-02-15 11:08:25 +00:00
00acafa290 fix docstring 2026-02-15 10:51:59 +00:00
f97d241f64 add linter exclusion for test files
set thresholds for issues
2026-02-15 09:54:12 +00:00
87ea26ab03 build-{OS} targets internal 2026-02-14 21:25:39 +00:00
331d4e2f9a add macos target to Taskfile+makefile 2026-02-14 21:18:31 +00:00
d21dfb9481 add 0.6.0 to CHANGELOG 2026-02-14 20:53:42 +00:00
510d43ca33 migrate golangci config to version 2.
update golang-cl workflow
2026-02-14 20:46:22 +00:00
acac22f70e add versionFromBuild() and sendCommands() to separate the logic in main() a little.
Flags now implements fmt.Stringer interface.

import lint fix
2026-02-14 20:45:13 +00:00
b568767f86 lint fix 2026-02-14 20:44:03 +00:00
08909b6b66 log at warn level invalid bps, channel values. inform user of default value. 2026-02-14 20:43:55 +00:00
c6d03e87d7 errors from newPacket() now propogated
Send() and Close() methods now use pointer receivers.
2026-02-14 20:42:33 +00:00
0d0dbedbcd used fixed-sized array for streamname instead of slice.
factory method now validates streamname size.

framecounter now stored as uint32. it's written to a byte buffer on each call to {Packet}.header()
add comment regarding framecounter wraparound.
2026-02-14 20:40:38 +00:00
github-actions[bot]
ecd16545ab chore: auto-update Go modules 2026-02-09 00:06:13 +00:00
github-actions[bot]
abdc50bf1f chore: auto-update Go modules 2026-02-02 01:04:10 +00:00
bf22881227 add chore: to changelog filter 2026-01-28 21:39:56 +00:00
github-actions[bot]
1473c53dea chore: auto-update Go modules 2026-01-26 00:04:13 +00:00
github-actions[bot]
6f10f3ce20 chore: auto-update Go modules 2026-01-19 00:04:12 +00:00
github-actions[bot]
f803a38619 chore: auto-update Go modules 2026-01-12 00:04:10 +00:00
github-actions[bot]
862f127d54 chore: auto-update Go modules 2025-12-22 00:04:48 +00:00
github-actions[bot]
500a1c886a chore: auto-update Go modules 2025-12-15 00:04:02 +00:00
github-actions[bot]
707f2c9a2c chore: auto-update Go modules 2025-12-01 00:04:22 +00:00
github-actions[bot]
9c9242bae4 chore: auto-update Go modules 2025-11-17 00:03:59 +00:00
github-actions[bot]
b56b4afcbf chore: auto-update Go modules 2025-11-10 00:04:01 +00:00
github-actions[bot]
c08019a063 chore: auto-update Go modules 2025-11-03 00:04:10 +00:00
github-actions[bot]
29a503ce8f chore: auto-update Go modules 2025-10-27 00:04:09 +00:00
github-actions[bot]
22529a0b39 chore: auto-update Go modules 2025-10-20 00:04:08 +00:00
github-actions[bot]
a153830d3f chore: auto-update Go modules 2025-10-13 00:03:57 +00:00
github-actions[bot]
e7736f383b chore: auto-update Go modules 2025-10-06 00:03:52 +00:00
github-actions[bot]
458ea95da0 chore: auto-update Go modules 2025-09-29 00:04:04 +00:00
github-actions[bot]
805a3250fc chore: auto-update Go modules 2025-09-15 00:04:06 +00:00
github-actions[bot]
defcbbc8a7 chore: auto-update Go modules 2025-09-08 00:03:47 +00:00
github-actions[bot]
19011b0b34 chore: auto-update Go modules 2025-08-25 00:03:51 +00:00
github-actions[bot]
923e741455 chore: auto-update Go modules 2025-08-18 00:03:57 +00:00
github-actions[bot]
5a0cf42347 chore: auto-update Go modules 2025-08-11 00:04:02 +00:00
github-actions[bot]
0f9dad3015 chore: auto-update Go modules 2025-07-21 00:04:25 +00:00
github-actions[bot]
9106a19486 chore: auto-update Go modules 2025-07-14 00:03:51 +00:00
github-actions[bot]
84ef1904fc chore: auto-update Go modules 2025-06-23 00:03:44 +00:00
f9ca8a05db add darwin + arm64 builds 2025-06-18 00:28:29 +01:00
9844d22c46 upd install instructions 2025-06-16 21:20:06 +01:00
github-actions[bot]
e7e158981b chore: auto-update Go modules 2025-06-16 00:03:45 +00:00
b60909e6ec typo 2025-06-15 07:50:11 +01:00
9a15ad98a8 fix flag name 2025-06-14 17:17:27 +01:00
b8fa4d5be0 fix env prefix 2025-06-14 17:05:16 +01:00
64c1cb8fbe update/reorganise readme 2025-06-14 17:03:49 +01:00
eeb789003b add changelog links 2025-06-14 09:16:19 +01:00
4fd190bc4e read host, streamname from env 2025-06-14 09:13:25 +01:00
c0aad67199 add 0.5.0 section to CHANGELOG 2025-06-14 09:05:34 +01:00
be65f41813 update Configuration section to reflect ff changes 2025-06-14 09:03:44 +01:00
fd72628530 inject version at build time 2025-06-14 09:00:04 +01:00
898fbc3ae2 implement flag parsing with flags first package
run it with the environment resolver

add --version/-v flag.
2025-06-14 08:59:05 +01:00
5b8219d107 swap out logrus for charmbracelet/log 2025-06-14 07:39:08 +01:00
8ab543df0f remove config/util 2025-06-14 07:38:33 +01:00
github-actions[bot]
2035e158b1 chore: auto-update Go modules 2025-05-12 00:03:44 +00:00
github-actions[bot]
6965b03ae5 chore: auto-update Go modules 2025-04-07 00:03:31 +00:00
09e4b107bf -log-level flag now -loglevel
Some checks failed
CI / Lint (push) Has been cancelled
Auto-Update Go Modules / update-go-modules (push) Has been cancelled
upd README, CHANGELOG
2025-04-05 22:15:29 +01:00
c7b9d75ea1 reword
Some checks failed
CI / Lint (push) Has been cancelled
2025-04-04 05:50:32 +01:00
cc42e928e0 upd tested against 2025-04-04 05:45:24 +01:00
80a675cc6a add/fix dates 2025-04-04 05:39:45 +01:00
6801d9e4e1 upd CHANGELOG 2025-04-04 05:38:46 +01:00
8aceb8229f wsl2 network mirroring now supports localhost. 2025-04-04 05:27:56 +01:00
b070fb615e log-level is now a str flag
add some validation logic

update Logging section in README

bps now defaults to 256000 (same as the vban-text-client)
see: https://github.com/vburel2018/VBAN-Text-Client
2025-04-04 05:27:26 +01:00
b9d0578507 prefer UserConfigDir
Some checks failed
CI / Lint (push) Has been cancelled
2025-03-31 20:57:47 +01:00
github-actions[bot]
438543b87b chore: auto-update Go modules
Some checks failed
Auto-Update Go Modules / update-go-modules (push) Has been cancelled
2025-03-24 00:03:34 +00:00
585b4d4c14 add golang-ci config + action
Some checks failed
CI / Lint (push) Has been cancelled
Auto-Update Go Modules / update-go-modules (push) Has been cancelled
2025-03-10 15:32:25 +00:00
29e6c2e8aa lint fixes 2025-03-10 15:32:10 +00:00
github-actions[bot]
7d93ecb1c7 chore: auto-update Go modules 2025-03-10 00:02:49 +00:00
github-actions[bot]
116ce30f53 chore: auto-update Go modules
Some checks failed
Auto-Update Go Modules / update-go-modules (push) Has been cancelled
2025-02-24 00:03:24 +00:00
4bb79c8500 add update and release actions
Some checks failed
Auto-Update Go Modules / update-go-modules (push) Has been cancelled
2025-02-17 13:13:44 +00:00
db497a017b run through formatter 2025-02-07 23:15:05 +00:00
3afc5ee66c add taskfile 2025-02-03 18:34:22 +00:00
99b630dd47 upd readme 2025-01-23 17:30:45 +00:00
8bd62b72d0 remove cat 2025-01-23 02:56:42 +00:00
e36af2c059 add 0.2.1 to changelog 2024-11-07 19:39:03 +00:00
5a5a6fa893 fix pointer 2024-11-07 19:36:56 +00:00
be11239d39 header now uses reusable buffer 2024-11-07 19:34:44 +00:00
d72c6a2d17 rename client struct to udpConn 2024-11-05 18:35:39 +00:00
c063feb919 fix default config.toml dir in README 2024-11-04 14:37:32 +00:00
ae170ca572 move indexOf into the vbantxt package.
improve the warning message on invalid bps value
2024-11-03 16:15:05 +00:00
7a844e3624 add note about functional options to README 2024-11-03 15:54:34 +00:00
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
23 changed files with 1248 additions and 233 deletions

30
.github/workflows/golang-ci.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: CI
on:
push:
branches: ['main']
paths:
- '**.go'
pull_request:
branches: ['main']
paths:
- '**.go'
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.6.0
args: --config .golangci.yml

31
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: goreleaser
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v5
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

30
.github/workflows/update-go-modules.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Auto-Update Go Modules
on:
schedule:
- cron: "0 0 * * 1" # Runs every Monday at midnight
jobs:
update-go-modules:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Update Dependencies
run: |
go get -u ./...
go mod tidy
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add go.mod go.sum
git commit -m "chore: auto-update Go modules"
git push

6
.gitignore vendored
View File

@@ -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
@@ -14,4 +15,7 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
*.txt # Added by goreleaser init:
dist/
.envrc

125
.golangci.yml Normal file
View File

@@ -0,0 +1,125 @@
version: '2'
run:
timeout: 3m
tests: true
go: '1.24'
linters:
enable:
# Default enabled linters
- errcheck # Check for unchecked errors
- govet # Go's built-in vetting tool
- ineffassign # Detect ineffectual assignments
- staticcheck # Advanced static analysis
- unused # Check for unused code
# Additional useful linters
- misspell # Detect common misspellings
- unparam # Check for unused function parameters
- gosec # Security checks
- asciicheck # Check for non-ASCII characters
- errname # Check error variable names
- godot # Check for missing periods in comments
- revive # Highly configurable linter for style and correctness
- gocritic # Detect code issues and suggest improvements
- gocyclo # Check for cyclomatic complexity
- dupl # Check for code duplication
- predeclared # Check for shadowing of predeclared identifiers
- copyloopvar # Check for loop variable capture in goroutines
- errorlint # Check for common mistakes in error handling
- goconst # Check for repeated strings that could be constants
- gosmopolitan # Check for non-portable code
settings:
misspell:
locale: UK
errcheck:
check-type-assertions: true
check-blank: true
revive:
rules:
# Code quality and style
- name: exported
arguments:
- 'checkPrivateReceivers'
- 'sayRepetitiveInsteadOfStutters'
- name: var-naming
- name: package-comments
- name: range-val-in-closure
- name: time-naming
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: empty-block
- name: error-return
- name: error-strings
- name: error-naming
- name: if-return
- name: increment-decrement
- name: indent-error-flow
- name: receiver-naming
- name: redefines-builtin-id
- name: superfluous-else
- name: unexported-return
- name: unreachable-code
- name: unused-parameter
- name: var-declaration
- name: blank-imports
- name: range
# Disabled rules (can be enabled if needed)
# - name: line-length-limit
# arguments: [120]
# - name: function-length
# arguments: [50, 0]
# - name: cyclomatic
# arguments: [10]
gosec:
excludes:
- G104 # Duplicated errcheck checks
- G115 # integer overflow conversion int -> uint32
exclusions:
warn-unused: false
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
# Formatters configuration
formatters:
# Enable specific formatters
enable:
- gofumpt # Stricter gofmt alternative
- goimports # Organizes imports
- gci # Controls import order/grouping
- golines # Enforces line length
# Formatter-specific settings
settings:
goimports:
local-prefixes: [github.com/onyx-and-iris/vbantxt]
gci:
# Define import sections order
sections:
- standard # Standard library
- default # Everything else
- prefix(github.com/onyx-and-iris/vbantxt) # Current module
gofumpt:
extra-rules: true # Enable additional formatting rules
issues:
# Limit the number of same issues reported to avoid spam
max-same-issues: 50
# Limit the number of issues per linter to keep output manageable
max-issues-per-linter: 100

58
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,58 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- main: ./cmd/vbantxt/
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
archives:
- formats: ['tar.gz']
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: ['zip']
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'
release:
footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).

90
CHANGELOG.md Normal file
View File

@@ -0,0 +1,90 @@
# 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.6.0] - 2025-02-14
### Added
- streamname length validation added to {packet} factory method.
### Changed
- {packet}.bspIndex now defaults to *indexOf(BpsOpts, 256000)*.
- this doesn't affect the CLI which already defaulted to 256000.
- WithBPSOpt and WithChannel functional options now log at warn level if passed out of bounds value. They will fallback to valid defaults.
# [0.5.0] - 2025-06-25
### Added
- it's now possible to load configuration from environment variables. See [Environment Variables](https://github.com/onyx-and-iris/vbantxt/tree/main?tab=readme-ov-file#environment-variables)
- --version/-v command
### Changed
- shortname for --host flag is now -H.
- shortname for --channel flag is now -n.
- toml loader no longer requires a `[connection]` table. See [TOML Config](https://github.com/onyx-and-iris/vbantxt/tree/main?tab=readme-ov-file#toml-config)
# [0.4.1] - 2025-04-05
### Changed
- `-loglevel` flag is now of type string. It accepts any one of trace, debug, info, warn, error, fatal or panic.
- It defaults to warn.
# [0.3.1] - 2025-03-31
### Fixed
- The CLI now uses `os.UserConfigDir()` to load the default *config.toml*, which should respect `$XDG_CONFIG_HOME`. See [UserConfigDir](https://pkg.go.dev/os#UserConfigDir)
# [0.2.1] - 2024-11-07
### Fixed
- {packet}.header() now uses a reusable buffer.
# [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
- Functional options `WithRateLimit` and `WithBPSOpt` and `WithChannel` added. Use them to configure the vbantxt client. See the [included vbantxt cli][vbantxt-cli] for an example of usage.
### 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.
[vbantxt-cli]: https://github.com/onyx-and-iris/vbantxt/blob/main/cmd/vbantxt/main.go

203
README.md
View File

@@ -1,48 +1,199 @@
# 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
## Tested against Send Voicemeeter/Matrix vban requests.
- Basic 1.0.8.4 For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
- Banana 2.0.6.4
- Potato 3.0.2.4 ---
## Table of Contents
- [Installation](#installation)
- [VBANTXT Package](#vbantxt-package)
- [VBANTXT CLI](#vbantxt-cli)
- [License](#license)
## 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 (a binary is available in [Releases](https://github.com/onyx-and-iris/vbantxt/releases))
## `Use` ## Tested against
#### Command Line - Basic 1.1.1.9
- Banana 2.1.1.9
- Potato 3.1.1.9
- Matrix 1.0.1.2
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 Package`
You may also store them in a `config.toml` located in `home directory / .vbantxt_cli /` ```console
go get github.com/onyx-and-iris/vbantxt
A valid `config.toml` might look like this:
```toml
[connection]
Host="gamepc.local"
Port=6990
Streamname="Command1"
``` ```
#### Script files ```go
package main
The vbantxt-cli utility accepts a single string request or an array of string requests. This means you can pass scripts stored in files. import (
"log"
"github.com/onyx-and-iris/vbantxt"
)
func main() {
var (
host string = "vm.local"
port int = 6980
streamname string = "onyx"
)
client, err := vbantxt.New(host, port, streamname)
if err != nil {
log.Fatal(err)
}
defer client.Close()
err = client.Send("strip[0].mute=0")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Error: %s", err)
os.Exit(1)
}
}
```
## `VBANTXT CLI`
### Installation
```console
go install github.com/onyx-and-iris/vbantxt/cmd/vbantxt@latest
```
### Use
Simply pass your vban commands as command line arguments:
```console
vbantxt "strip[0].mute=1 strip[1].mono=1"
```
### Configuration
#### Flags
```console
FLAGS
-H, --host STRING VBAN host (default: localhost)
-p, --port INT VBAN port (default: 6980)
-s, --streamname STRING VBAN stream name (default: Command1)
-b, --bps INT VBAN BPS (default: 256000)
-n, --channel INT VBAN channel (default: 0)
-r, --ratelimit INT VBAN rate limit (ms) (default: 20)
-C, --config STRING Path to the configuration file (default: $XDG_CONFIG_HOME/vbantxt/config.toml)
-l, --loglevel STRING Log level (debug, info, warn, error, fatal, panic) (default: warn)
-v, --version Show version information
```
Pass --host, --port and --streamname as flags on the root command, for example:
```console
vbantxt --host=localhost --port=6980 --streamname=Command1 --help
```
#### Environment Variables
All flags have corresponding environment variables, prefixed with `VBANTXT_`:
```bash
#!/usr/bin/env bash
export VBANTXT_HOST=localhost
export VBANTXT_PORT=6980
export VBANTXT_STREAMNAME=Command1
```
Flags will override environment variables.
#### TOML Config
By default the config loader will look for a config in:
- $XDG_CONFIG_HOME / vbantxt / config.toml (see [os.UserConfigDir](https://pkg.go.dev/os#UserConfigDir))
- A custom config path may be passed with the --config/-C flag.
All flags have corresponding keys in the config file, for example:
```toml
host="gamepc.local"
port=6980
streamname="Command1"
```
---
## `Script files`
The vbantxt CLI accepts a single string request or an array of string requests. This means you can pass scripts stored in files.
For example, in Windows with Powershell you could: For example, in Windows with Powershell you could:
`vbantxt-cli $(Get-Content .\script.txt)` ```console
vbantxt $(Get-Content .\script.txt)
```
Or with Bash:
```console
xargs vbantxt < script.txt
```
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:
```console
vbantxt "Point(ASIO128.IN[2],ASIO128.OUT[1]).dBGain = -8"
```
---
## `Logging`
The --loglevel/-l flag allows you to control the verbosity of the application's logging output.
Acceptable values for this flag are:
- `debug`
- `info`
- `warn`
- `error`
- `fatal`
- `panic`
For example, to set the log level to `debug`, you can use:
```console
vbantxt --loglevel=debug "bus[0].eq.on=1 bus[1].gain=-12.8"
```
The default log level is `warn` if the flag is not specified.
## License
`vbantxt` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.

64
Taskfile.yml Normal file
View File

@@ -0,0 +1,64 @@
version: '3'
vars:
PROGRAM: vbantxt
SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}'
BIN_DIR: bin
VERSION:
sh: 'git describe --tags $(git rev-list --tags --max-count=1)'
WINDOWS: '{{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe'
LINUX: '{{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64'
MACOS: '{{.BIN_DIR}}/{{.PROGRAM}}_darwin_amd64'
tasks:
default:
desc: Build the vbantxt project
cmds:
- task: build
build:
desc: 'Build the vbantxt project'
deps: [vet]
cmds:
- task: build-windows
- task: build-linux
- task: build-macos
vet:
desc: Vet the code
deps: [fmt]
cmds:
- go vet ./...
fmt:
desc: Fmt the code
cmds:
- go fmt ./...
build-windows:
desc: Build the vbantxt project for Windows
cmds:
- GOOS=windows GOARCH=amd64 go build -o {{.WINDOWS}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
internal: true
build-linux:
desc: Build the vbantxt project for Linux
cmds:
- GOOS=linux GOARCH=amd64 go build -o {{.LINUX}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
internal: true
build-macos:
desc: Build the vbantxt project for macOS
cmds:
- GOOS=darwin GOARCH=amd64 go build -o {{.MACOS}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
internal: true
test:
desc: Run tests
cmds:
- go test ./...
clean:
desc: Clean the build artifacts
cmds:
- '{{.SHELL}} rm -r {{.BIN_DIR}}'

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

@@ -0,0 +1,190 @@
// Package main implements a command-line tool for sending text messages over VBAN using the vbantxt library.
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/peterbourgon/ff/v4"
"github.com/peterbourgon/ff/v4/ffhelp"
"github.com/peterbourgon/ff/v4/fftoml"
"github.com/onyx-and-iris/vbantxt"
)
var version string // Version will be set at build time
// Flags holds the command-line flags for the VBANTXT client.
type Flags struct {
Host string
Port int
Streamname string
Bps int
Channel int
Ratelimit int
ConfigPath string // Path to the configuration file
Loglevel string // Log level
Version bool // Version flag
}
func (f *Flags) String() string {
return fmt.Sprintf(
"Host: %s, Port: %d, Streamname: %s, Bps: %d, Channel: %d, Ratelimit: %dms, ConfigPath: %s, Loglevel: %s",
f.Host,
f.Port,
f.Streamname,
f.Bps,
f.Channel,
f.Ratelimit,
f.ConfigPath,
f.Loglevel,
)
}
func main() {
var exitCode int
// Defer exit with the final exit code
defer func() {
if exitCode != 0 {
os.Exit(exitCode)
}
}()
closer, err := run()
if closer != nil {
defer closer()
}
if err != nil {
log.Error(err)
exitCode = 1
}
}
// run contains the main application logic and returns a closer function and any error.
func run() (func(), error) {
var flags Flags
// VBAN specific flags
fs := ff.NewFlagSet("vbantxt - A command-line tool for sending text requests over VBAN")
fs.StringVar(&flags.Host, 'H', "host", "localhost", "VBAN host")
fs.IntVar(&flags.Port, 'p', "port", 6980, "VBAN port")
fs.StringVar(&flags.Streamname, 's', "streamname", "Command1", "VBAN stream name")
fs.IntVar(&flags.Bps, 'b', "bps", 256000, "VBAN BPS")
fs.IntVar(&flags.Channel, 'n', "channel", 0, "VBAN channel")
fs.IntVar(&flags.Ratelimit, 'r', "ratelimit", 20, "VBAN rate limit (ms)")
configDir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("failed to get user config directory: %w", err)
}
defaultConfigPath := filepath.Join(configDir, "vbantxt", "config.toml")
// Configuration file and logging flags
fs.StringVar(
&flags.ConfigPath,
'C',
"config",
defaultConfigPath,
"Path to the configuration file",
)
fs.StringVar(
&flags.Loglevel,
'l',
"loglevel",
"warn",
"Log level (debug, info, warn, error, fatal, panic)",
)
fs.BoolVar(&flags.Version, 'v', "version", "Show version information")
err = ff.Parse(fs, os.Args[1:],
ff.WithEnvVarPrefix("VBANTXT"),
ff.WithConfigFileFlag("config"),
ff.WithConfigAllowMissingFile(),
ff.WithConfigFileParser(fftoml.Parser{Delimiter: "."}.Parse),
)
switch {
case errors.Is(err, ff.ErrHelp):
fmt.Fprintf(os.Stderr, "%s\n", ffhelp.Flags(fs, "vbantxt [flags] <vban commands>"))
os.Exit(0)
case err != nil:
return nil, fmt.Errorf("failed to parse flags: %w", err)
}
if flags.Version {
fmt.Printf("vbantxt version: %s\n", versionFromBuild())
os.Exit(0)
}
level, err := log.ParseLevel(flags.Loglevel)
if err != nil {
return nil, fmt.Errorf("invalid log level %q", flags.Loglevel)
}
log.SetLevel(level)
log.Debugf("Loaded configuration: %s", flags.String())
client, closer, err := createClient(&flags)
if err != nil {
return nil, fmt.Errorf("failed to create VBAN client: %w", err)
}
commands := fs.GetArgs()
if len(commands) == 0 {
return closer, errors.New(
"no VBAN commands provided; please provide at least one command as an argument",
)
}
sendCommands(client, commands)
return closer, nil
}
// versionFromBuild retrieves the version information from the build metadata.
func versionFromBuild() string {
if version == "" {
info, ok := debug.ReadBuildInfo()
if !ok {
return "(unable to read build info)"
}
version = strings.Split(info.Main.Version, "-")[0]
}
return version
}
// createClient creates a new vban client with the provided options.
func createClient(flags *Flags) (*vbantxt.VbanTxt, func(), error) {
client, err := vbantxt.New(
flags.Host,
flags.Port,
flags.Streamname,
vbantxt.WithBPSOpt(flags.Bps),
vbantxt.WithChannel(flags.Channel),
vbantxt.WithRateLimit(time.Duration(flags.Ratelimit)*time.Millisecond))
if err != nil {
return nil, nil, err
}
closer := func() {
if err := client.Close(); err != nil {
log.Error(err)
}
}
return client, closer, err
}
// sendCommands sends the provided VBAN commands using the client and logs any errors that occur.
func sendCommands(client *vbantxt.VbanTxt, commands []string) {
for _, cmd := range commands {
if err := client.Send(cmd); err != nil {
log.Errorf("Failed to send command '%s': %v", cmd, err)
}
}
}

10
errors.go Normal file
View File

@@ -0,0 +1,10 @@
// Package vbantxt provides utilities for handling VBAN text errors.
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)
}

36
go.mod
View File

@@ -1,10 +1,36 @@
module github.com/onyx-and-iris/vbantxt-cli module github.com/onyx-and-iris/vbantxt
go 1.19 go 1.24.2
require ( require (
github.com/BurntSushi/toml v1.2.1 github.com/charmbracelet/log v0.4.2
github.com/sirupsen/logrus v1.9.0 github.com/peterbourgon/ff/v4 v4.0.0-beta.1
github.com/stretchr/testify v1.10.0
) )
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/sys v0.41.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

72
go.sum
View File

@@ -1,17 +1,71 @@
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc=
github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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
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
}

46
makefile Normal file
View File

@@ -0,0 +1,46 @@
PROGRAM = vbantxt
GO = go
BIN_DIR := bin
WINDOWS=$(BIN_DIR)/$(PROGRAM)_windows_amd64.exe
LINUX=$(BIN_DIR)/$(PROGRAM)_linux_amd64
MACOS=$(BIN_DIR)/$(PROGRAM)_darwin_amd64
VERSION=$(shell git describe --tags $(shell git rev-list --tags --max-count=1))
.DEFAULT_GOAL := build
.PHONY: fmt vet build windows linux macos test clean
fmt:
$(GO) fmt ./...
vet: fmt
$(GO) vet ./...
build: vet windows linux macos | $(BIN_DIR)
@echo version: $(VERSION)
windows: $(WINDOWS)
linux: $(LINUX)
macos: $(MACOS)
$(WINDOWS):
env GOOS=windows GOARCH=amd64 go build -v -o $(WINDOWS) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/$(PROGRAM)/
$(LINUX):
env GOOS=linux GOARCH=amd64 go build -v -o $(LINUX) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/$(PROGRAM)/
$(MACOS):
env GOOS=darwin GOARCH=amd64 go build -v -o $(MACOS) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/$(PROGRAM)/
test:
$(GO) test ./...
$(BIN_DIR):
@mkdir -p $@
clean:
@rm -rv $(BIN_DIR)

55
option.go Normal file
View File

@@ -0,0 +1,55 @@
package vbantxt
import (
"time"
"github.com/charmbracelet/log"
)
// 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 {VbanTxt}.packet.
func WithBPSOpt(bps int) Option {
return func(vt *VbanTxt) {
defaultBps := BpsOpts[vt.packet.bpsIndex]
bpsIndex := indexOf(BpsOpts, bps)
if bpsIndex == -1 {
log.Warnf(
"invalid bps value %d, expected one of %v, defaulting to %d",
bps,
BpsOpts,
defaultBps,
)
return
}
if bpsIndex > 255 {
log.Warnf("bps index %d too large for uint8, defaulting to %d", bpsIndex, defaultBps)
return
}
vt.packet.bpsIndex = uint8(bpsIndex)
}
}
// WithChannel is a functional option to set the channel for {VbanTxt}.packet.
func WithChannel(channel int) Option {
return func(vt *VbanTxt) {
if channel < 0 || channel > 255 {
log.Warnf(
"channel value %d out of range [0,255], defaulting to %d",
channel,
vt.packet.channel,
)
return
}
vt.packet.channel = uint8(channel)
}
}

116
packet.go
View File

@@ -1,50 +1,94 @@
package main package vbantxt
var r *requestHeader import (
"bytes"
"encoding/binary"
"fmt"
const VBAN_PROTOCOL_TXT = 0x40 "github.com/charmbracelet/log"
)
// requestHeader represents a single request header const (
type requestHeader struct { vbanProtocolTxt = 0x40
name string streamNameSz = 16
bpsIndex int headerSz = 4 + 1 + 1 + 1 + 1 + 16 + 4
channel int )
framecounter []byte
// BpsOpts defines the available baud rate options.
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,
} }
// newRequestHeader returns a pointer to a requestHeader struct as a singleton type packet struct {
func newRequestHeader(streamname string, bpsI, channel int) *requestHeader { streamname [streamNameSz]byte
if r != nil { bpsIndex uint8
return r channel uint8
} framecounter uint32
return &requestHeader{streamname, bpsI, channel, make([]byte, 4)} hbuf *bytes.Buffer
} }
// sr defines the samplerate for the request // newPacket creates a new packet with the given stream name and default values for other fields.
func (r *requestHeader) sr() byte { // It validates the stream name length and ensures the default baud rate is present in BpsOpts.
return byte(VBAN_PROTOCOL_TXT + r.bpsIndex) func newPacket(streamname string) (packet, error) {
if len(streamname) > streamNameSz {
return packet{}, fmt.Errorf(
"streamname too long: %d chars, max %d",
len(streamname),
streamNameSz,
)
} }
// nbc defines the channel of the request var streamnameBuf [streamNameSz]byte
func (r *requestHeader) nbc() byte { copy(streamnameBuf[:], streamname)
return byte(r.channel)
bpsIndex := indexOf(BpsOpts, 256000)
if bpsIndex == -1 {
return packet{}, fmt.Errorf("default baud rate 256000 not found in BpsOpts")
} }
// streamname defines the stream name of the text request return packet{
func (r *requestHeader) streamname() []byte { streamname: streamnameBuf,
b := make([]byte, 16) bpsIndex: uint8(bpsIndex),
copy(b, r.name) channel: 0,
return b framecounter: 0,
hbuf: bytes.NewBuffer(make([]byte, 0, headerSz)),
}, nil
} }
// header returns a fully formed text request packet header // sr defines the samplerate for the request.
func (t *requestHeader) header() []byte { func (p *packet) sr() byte {
h := []byte("VBAN") return byte(vbanProtocolTxt + p.bpsIndex)
h = append(h, t.sr()) }
h = append(h, byte(0))
h = append(h, t.nbc()) // nbc defines the channel of the request.
h = append(h, byte(0x10)) func (p *packet) nbc() byte {
h = append(h, t.streamname()...) return byte(p.channel)
h = append(h, t.framecounter...) }
return h
// header returns a fully formed packet header.
func (p *packet) header() []byte {
p.hbuf.Reset()
p.hbuf.WriteString("VBAN")
p.hbuf.WriteByte(p.sr())
p.hbuf.WriteByte(byte(0))
p.hbuf.WriteByte(p.nbc())
p.hbuf.WriteByte(byte(0x10))
p.hbuf.Write(p.streamname[:])
var frameBytes [4]byte
binary.LittleEndian.PutUint32(frameBytes[:], p.framecounter)
p.hbuf.Write(frameBytes[:])
return p.hbuf.Bytes()
}
// bumpFrameCounter increments the frame counter by 1.
// The uint32 will safely wrap to 0 after reaching max value (4,294,967,295),
// which is expected behaviour for network protocol sequence numbers.
func (p *packet) bumpFrameCounter() {
p.framecounter++
log.Debugf("framecounter: %d", p.framecounter)
} }

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

48
udpconn.go Normal file
View File

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

View File

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

64
vbantxt.go Normal file
View File

@@ -0,0 +1,64 @@
package vbantxt
import (
"fmt"
"io"
"time"
)
// VbanTxt is used to send VBAN-TXT requests to a distant Voicemeeter/Matrix.
type VbanTxt struct {
conn 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) {
conn, err := newUDPConn(host, port)
if err != nil {
return nil, fmt.Errorf("error creating UDP client for (%s:%d): %w", host, port, err)
}
packet, err := newPacket(streamname)
if err != nil {
return nil, fmt.Errorf("error creating packet: %w", err)
}
vt := &VbanTxt{
conn: conn,
packet: packet,
ratelimit: time.Duration(20) * time.Millisecond,
}
for _, o := range options {
o(vt)
}
return vt, nil
}
// Send is responsible for firing each VBAN-TXT request.
// It waits for {vt.ratelimit} time before returning.
func (vt *VbanTxt) Send(cmd string) error {
_, err := vt.conn.Write(append(vt.packet.header(), 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.conn.Close()
if err != nil {
return fmt.Errorf("error attempting to close UDP Client: %w", err)
}
return nil
}

44
vbantxt_test.go Normal file
View File

@@ -0,0 +1,44 @@
package vbantxt_test
import (
"bufio"
"bytes"
_ "embed"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/onyx-and-iris/vbantxt"
)
//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(os.Getenv("VBANTXT_HOST"), 6980, os.Getenv("VBANTXT_STREAMNAME"))
require.NoError(t, err)
run(t, client, vm)
}
func TestSendMatrix(t *testing.T) {
client, err := vbantxt.New(os.Getenv("VBANTXT_HOST"), 6990, os.Getenv("VBANTXT_STREAMNAME"))
require.NoError(t, err)
run(t, client, matrix)
}