mirror of
https://github.com/onyx-and-iris/xair-cli.git
synced 2026-02-03 23:17:47 +00:00
initial commit
add internal/xair module
This commit is contained in:
commit
4a6ace0fdf
28
.github/workflows/release.yml
vendored
Normal file
28
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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
30
.github/workflows/update-go-modules.yml
vendored
Normal 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
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by ignr: github.com/onyx-and-iris/ignr
|
||||||
|
|
||||||
|
## Go ##
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Code coverage profiles and other test artifacts
|
||||||
|
*.out
|
||||||
|
coverage.*
|
||||||
|
*.coverprofile
|
||||||
|
profile.cov
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
|
# .idea/
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# End of ignr
|
||||||
55
.goreleaser.yml
Normal file
55
.goreleaser.yml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# 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:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
|
||||||
|
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).
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Xair-CLI
|
||||||
|
|
||||||
|
### Use
|
||||||
|
|
||||||
|
```console
|
||||||
|
xair-cli is a command-line tool that allows users to send OSC messages
|
||||||
|
to Behringer X Air mixers for remote control and configuration. It supports
|
||||||
|
various commands to manage mixer settings directly from the terminal.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
xair-cli [flags]
|
||||||
|
xair-cli [command]
|
||||||
|
|
||||||
|
Available Commands:
|
||||||
|
bus A brief description of your command
|
||||||
|
completion Generate the autocompletion script for the specified shell
|
||||||
|
help Help about any command
|
||||||
|
main A brief description of your command
|
||||||
|
strip A brief description of your command
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for xair-cli
|
||||||
|
-H, --host string host address of the X Air mixer (default "mixer.local")
|
||||||
|
-l, --loglevel string Log level (debug, info, warn, error, fatal, panic) (default "info")
|
||||||
|
-p, --port int Port number of the X Air mixer (default 10024)
|
||||||
|
|
||||||
|
Use "xair-cli [command] --help" for more information about a command.
|
||||||
|
```
|
||||||
40
go.mod
Normal file
40
go.mod
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
module github.com/onyx-and-iris/xair-cli
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/log v0.4.2
|
||||||
|
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
)
|
||||||
88
go.sum
Normal file
88
go.sum
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
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.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ=
|
||||||
|
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
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=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
276
internal/xair/client.go
Normal file
276
internal/xair/client.go
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
package xair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
|
||||||
|
"github.com/hypebeast/go-osc/osc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type parser interface {
|
||||||
|
Parse(data []byte) (*osc.Message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type XAirClient struct {
|
||||||
|
conn *net.UDPConn
|
||||||
|
mixerAddr *net.UDPAddr
|
||||||
|
|
||||||
|
parser parser
|
||||||
|
|
||||||
|
done chan bool
|
||||||
|
respChan chan *osc.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new XAirClient instance
|
||||||
|
func NewClient(mixerIP string, mixerPort int) (*XAirClient, error) {
|
||||||
|
localAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", 0))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve local address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.ListenUDP("udp", localAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create UDP connection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mixerAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", mixerIP, mixerPort))
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to resolve mixer address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Local UDP connection: %s ", conn.LocalAddr().String())
|
||||||
|
|
||||||
|
return &XAirClient{
|
||||||
|
conn: conn,
|
||||||
|
mixerAddr: mixerAddr,
|
||||||
|
parser: newParser(),
|
||||||
|
done: make(chan bool),
|
||||||
|
respChan: make(chan *osc.Message),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins listening for messages in a goroutine
|
||||||
|
func (x *XAirClient) StartListening() {
|
||||||
|
go x.receiveLoop()
|
||||||
|
log.Debugf("Started listening on %s...", x.conn.LocalAddr().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiveLoop handles incoming OSC messages
|
||||||
|
func (x *XAirClient) receiveLoop() {
|
||||||
|
buffer := make([]byte, 4096)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-x.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Set read timeout to avoid blocking forever
|
||||||
|
x.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||||
|
|
||||||
|
n, _, err := x.conn.ReadFromUDP(buffer)
|
||||||
|
if err != nil {
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
// Timeout is expected, continue loop
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check if we're shutting down to avoid logging expected errors
|
||||||
|
select {
|
||||||
|
case <-x.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
log.Errorf("Read error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := x.parseOSCMessage(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to parse OSC message: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x.respChan <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOSCMessage parses raw bytes into an OSC message with improved error handling
|
||||||
|
func (x *XAirClient) parseOSCMessage(data []byte) (*osc.Message, error) {
|
||||||
|
msg, err := x.parser.Parse(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the client and closes the connection
|
||||||
|
func (x *XAirClient) Stop() {
|
||||||
|
close(x.done)
|
||||||
|
if x.conn != nil {
|
||||||
|
x.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessage sends an OSC message to the mixer using the unified connection
|
||||||
|
func (x *XAirClient) SendMessage(address string, args ...any) error {
|
||||||
|
return x.SendToAddress(x.mixerAddr, address, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendToAddress sends an OSC message to a specific address (enables replying to different ports)
|
||||||
|
func (x *XAirClient) SendToAddress(addr *net.UDPAddr, oscAddress string, args ...any) error {
|
||||||
|
msg := osc.NewMessage(oscAddress)
|
||||||
|
for _, arg := range args {
|
||||||
|
msg.Append(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Sending to %v: %s", addr, msg.String())
|
||||||
|
if len(args) > 0 {
|
||||||
|
log.Debug(" - Arguments: ")
|
||||||
|
for i, arg := range args {
|
||||||
|
if i > 0 {
|
||||||
|
log.Debug(", ")
|
||||||
|
}
|
||||||
|
log.Debugf("%v", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debug("")
|
||||||
|
|
||||||
|
data, err := msg.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = x.conn.WriteToUDP(data, addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestInfo requests mixer information
|
||||||
|
func (x *XAirClient) RequestInfo() (error, InfoResponse) {
|
||||||
|
err := x.SendMessage("/xinfo")
|
||||||
|
if err != nil {
|
||||||
|
return err, InfoResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
val := <-x.respChan
|
||||||
|
var info InfoResponse
|
||||||
|
if len(val.Arguments) >= 3 {
|
||||||
|
info.Host = val.Arguments[0].(string)
|
||||||
|
info.Name = val.Arguments[1].(string)
|
||||||
|
info.Model = val.Arguments[2].(string)
|
||||||
|
}
|
||||||
|
return nil, info
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeepAlive sends keep-alive message (required for multi-client usage)
|
||||||
|
func (x *XAirClient) KeepAlive() error {
|
||||||
|
return x.SendMessage("/xremote")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestStatus requests mixer status
|
||||||
|
func (x *XAirClient) RequestStatus() error {
|
||||||
|
return x.SendMessage("/status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChannelGain sets gain for a specific channel (1-based indexing)
|
||||||
|
func (x *XAirClient) SetChannelGain(channel int, gain float32) error {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/mix/fader", channel)
|
||||||
|
return x.SendMessage(address, gain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MuteChannel mutes/unmutes a specific channel (1-based indexing)
|
||||||
|
func (x *XAirClient) MuteChannel(channel int, muted bool) error {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/mix/on", channel)
|
||||||
|
var value int32 = 0
|
||||||
|
if !muted {
|
||||||
|
value = 1
|
||||||
|
}
|
||||||
|
return x.SendMessage(address, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMainLRFader requests the current main L/R fader level
|
||||||
|
func (x *XAirClient) GetMainLRFader() (float64, error) {
|
||||||
|
err := x.SendMessage("/lr/mix/fader")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := <-x.respChan
|
||||||
|
val, ok := resp.Arguments[0].(float32)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("unexpected argument type for main LR fader value")
|
||||||
|
}
|
||||||
|
return mustDbFrom(float64(val)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMainLRFader sets the main L/R fader level
|
||||||
|
func (x *XAirClient) SetMainLRFader(level float64) error {
|
||||||
|
return x.SendMessage("/lr/mix/fader", float32(mustDbInto(level)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelFader requests the current fader level for a channel
|
||||||
|
func (x *XAirClient) GetChannelFader(channel int) (float64, error) {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/mix/fader", channel)
|
||||||
|
err := x.SendMessage(address)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := <-x.respChan
|
||||||
|
val, ok := resp.Arguments[0].(float32)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("unexpected argument type for fader value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mustDbFrom(float64(val)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelMute requests the current mute state for a channel
|
||||||
|
func (x *XAirClient) GetChannelMute(channel int) error {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/mix/on", channel)
|
||||||
|
return x.SendMessage(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChannelName sets the name for a specific channel
|
||||||
|
func (x *XAirClient) SetChannelName(channel int, name string) error {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/config/name", channel)
|
||||||
|
return x.SendMessage(address, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelName requests the name for a specific channel
|
||||||
|
func (x *XAirClient) GetChannelName(channel int) error {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/config/name", channel)
|
||||||
|
return x.SendMessage(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelColor requests the color for a specific channel
|
||||||
|
func (x *XAirClient) GetChannelColor(channel int) error {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/config/color", channel)
|
||||||
|
return x.SendMessage(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChannelColor sets the color for a specific channel (0-15)
|
||||||
|
func (x *XAirClient) SetChannelColor(channel int, color int32) error {
|
||||||
|
address := fmt.Sprintf("/ch/%02d/config/color", channel)
|
||||||
|
return x.SendMessage(address, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllChannelInfo requests information for all channels
|
||||||
|
func (x *XAirClient) GetAllChannelInfo(maxChannels int) error {
|
||||||
|
fmt.Printf("\n=== REQUESTING ALL CHANNEL INFO (1-%d) ===\n", maxChannels)
|
||||||
|
for ch := 1; ch <= maxChannels; ch++ {
|
||||||
|
fmt.Printf("Requesting info for channel %d...\n", ch)
|
||||||
|
x.GetChannelName(ch)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
x.GetChannelFader(ch)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
x.GetChannelMute(ch)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
x.GetChannelColor(ch)
|
||||||
|
time.Sleep(200 * time.Millisecond) // Longer pause between channels
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
7
internal/xair/models.go
Normal file
7
internal/xair/models.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package xair
|
||||||
|
|
||||||
|
type InfoResponse struct {
|
||||||
|
Host string
|
||||||
|
Name string
|
||||||
|
Model string
|
||||||
|
}
|
||||||
213
internal/xair/parser.go
Normal file
213
internal/xair/parser.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package xair
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/hypebeast/go-osc/osc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type xairParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParser() *xairParser {
|
||||||
|
return &xairParser{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOSCMessage parses raw bytes into an OSC message with improved error handling
|
||||||
|
func (p *xairParser) Parse(data []byte) (*osc.Message, error) {
|
||||||
|
log.Debug("=== PARSING OSC MESSAGE BEGIN ===")
|
||||||
|
defer log.Debug("=== PARSING OSC MESSAGE END ===")
|
||||||
|
|
||||||
|
if err := p.validateOSCData(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
address, addressEnd, err := p.extractOSCAddress(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := osc.NewMessage(address)
|
||||||
|
|
||||||
|
typeTags, typeTagsEnd, err := p.extractOSCTypeTags(data, addressEnd)
|
||||||
|
if err != nil || typeTags == "" {
|
||||||
|
log.Debug("No valid type tags, returning address-only message")
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.parseOSCArguments(data, typeTagsEnd, typeTags, msg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Successfully parsed message with %d arguments", len(msg.Arguments))
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateOSCData performs basic validation on OSC message data
|
||||||
|
func (p *xairParser) validateOSCData(data []byte) error {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return fmt.Errorf("data too short for OSC message")
|
||||||
|
}
|
||||||
|
if data[0] != '/' {
|
||||||
|
return fmt.Errorf("invalid OSC message: does not start with '/'")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractOSCAddress extracts the OSC address from the message data
|
||||||
|
func (p *xairParser) extractOSCAddress(data []byte) (address string, nextPos int, err error) {
|
||||||
|
nullPos := bytes.IndexByte(data, 0)
|
||||||
|
if nullPos <= 0 {
|
||||||
|
return "", 0, fmt.Errorf("no null terminator found for address")
|
||||||
|
}
|
||||||
|
|
||||||
|
address = string(data[:nullPos])
|
||||||
|
log.Debugf("Parsed OSC address: %s", address)
|
||||||
|
|
||||||
|
// Calculate next 4-byte aligned position
|
||||||
|
nextPos = ((nullPos + 4) / 4) * 4
|
||||||
|
return address, nextPos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractOSCTypeTags extracts and validates OSC type tags
|
||||||
|
func (p *xairParser) extractOSCTypeTags(data []byte, start int) (typeTags string, nextPos int, err error) {
|
||||||
|
if start >= len(data) {
|
||||||
|
return "", start, nil // No type tags available
|
||||||
|
}
|
||||||
|
|
||||||
|
typeTagsEnd := bytes.IndexByte(data[start:], 0)
|
||||||
|
if typeTagsEnd <= 0 {
|
||||||
|
return "", start, nil // No type tags found
|
||||||
|
}
|
||||||
|
|
||||||
|
typeTags = string(data[start : start+typeTagsEnd])
|
||||||
|
log.Debugf("Parsed type tags: %s", typeTags)
|
||||||
|
|
||||||
|
if len(typeTags) == 0 || typeTags[0] != ',' {
|
||||||
|
log.Debug("Invalid type tags format")
|
||||||
|
return "", start, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate arguments start position (4-byte aligned)
|
||||||
|
nextPos = ((start + typeTagsEnd + 4) / 4) * 4
|
||||||
|
return typeTags, nextPos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOSCArguments parses OSC arguments based on type tags
|
||||||
|
func (p *xairParser) parseOSCArguments(data []byte, argsStart int, typeTags string, msg *osc.Message) error {
|
||||||
|
argData := data[argsStart:]
|
||||||
|
argNum := 0
|
||||||
|
|
||||||
|
for i := 1; i < len(typeTags) && len(argData) > 0; i++ {
|
||||||
|
var consumed int
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch typeTags[i] {
|
||||||
|
case 's':
|
||||||
|
consumed, err = p.parseStringArgument(argData, msg, argNum)
|
||||||
|
case 'i':
|
||||||
|
consumed, err = p.parseInt32Argument(argData, msg, argNum)
|
||||||
|
case 'f':
|
||||||
|
consumed, err = p.parseFloat32Argument(argData, msg, argNum)
|
||||||
|
case 'b':
|
||||||
|
consumed, err = p.parseBlobArgument(argData, msg, argNum)
|
||||||
|
default:
|
||||||
|
log.Debugf("Unknown type tag: %c (skipping)", typeTags[i])
|
||||||
|
consumed = p.skipUnknownArgument(argData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Error parsing argument %d: %v", argNum+1, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if consumed == 0 {
|
||||||
|
break // No more data to consume
|
||||||
|
}
|
||||||
|
|
||||||
|
argData = argData[consumed:]
|
||||||
|
if typeTags[i] != '?' { // Don't count skipped arguments
|
||||||
|
argNum++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStringArgument parses a string argument from OSC data
|
||||||
|
func (p *xairParser) parseStringArgument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
||||||
|
nullPos := bytes.IndexByte(data, 0)
|
||||||
|
if nullPos < 0 {
|
||||||
|
return 0, fmt.Errorf("no null terminator found for string")
|
||||||
|
}
|
||||||
|
|
||||||
|
argStr := string(data[:nullPos])
|
||||||
|
log.Debugf("Parsed string argument %d: %s", argNum+1, argStr)
|
||||||
|
msg.Append(argStr)
|
||||||
|
|
||||||
|
// Return next 4-byte aligned position
|
||||||
|
return ((nullPos + 4) / 4) * 4, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt32Argument parses an int32 argument from OSC data
|
||||||
|
func (p *xairParser) parseInt32Argument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return 0, fmt.Errorf("insufficient data for int32")
|
||||||
|
}
|
||||||
|
|
||||||
|
val := int32(binary.BigEndian.Uint32(data[:4]))
|
||||||
|
log.Debugf("Parsed int32 argument %d: %d", argNum+1, val)
|
||||||
|
msg.Append(val)
|
||||||
|
|
||||||
|
return 4, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFloat32Argument parses a float32 argument from OSC data
|
||||||
|
func (p *xairParser) parseFloat32Argument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return 0, fmt.Errorf("insufficient data for float32")
|
||||||
|
}
|
||||||
|
|
||||||
|
val := math.Float32frombits(binary.BigEndian.Uint32(data[:4]))
|
||||||
|
log.Debugf("Parsed float32 argument %d: %f", argNum+1, val)
|
||||||
|
msg.Append(val)
|
||||||
|
|
||||||
|
return 4, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBlobArgument parses a blob argument from OSC data
|
||||||
|
func (p *xairParser) parseBlobArgument(data []byte, msg *osc.Message, argNum int) (int, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return 0, fmt.Errorf("insufficient data for blob size")
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int32(binary.BigEndian.Uint32(data[:4]))
|
||||||
|
if size < 0 || size >= 10000 {
|
||||||
|
return 0, fmt.Errorf("invalid blob size: %d", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < int(4+size) {
|
||||||
|
return 0, fmt.Errorf("insufficient data for blob content")
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := make([]byte, size)
|
||||||
|
copy(blob, data[4:4+size])
|
||||||
|
log.Debugf("Parsed blob argument %d (%d bytes)", argNum+1, size)
|
||||||
|
msg.Append(blob)
|
||||||
|
|
||||||
|
// Return next 4-byte aligned position
|
||||||
|
return ((4 + int(size) + 3) / 4) * 4, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipUnknownArgument skips an unknown argument type
|
||||||
|
func (p *xairParser) skipUnknownArgument(data []byte) int {
|
||||||
|
// Skip unknown types by moving 4 bytes if available
|
||||||
|
if len(data) >= 4 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
42
internal/xair/util.go
Normal file
42
internal/xair/util.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package xair
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
func mustDbInto(db float64) float64 {
|
||||||
|
switch {
|
||||||
|
case db >= 10:
|
||||||
|
return 1
|
||||||
|
case db >= -10:
|
||||||
|
return float64((db + 30) / 40)
|
||||||
|
case db >= -30:
|
||||||
|
return float64((db + 50) / 80)
|
||||||
|
case db >= -60:
|
||||||
|
return float64((db + 70) / 160)
|
||||||
|
case db >= -90:
|
||||||
|
return float64((db + 90) / 480)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustDbFrom(level float64) float64 {
|
||||||
|
switch {
|
||||||
|
case level >= 1:
|
||||||
|
return 10
|
||||||
|
case level >= 0.5:
|
||||||
|
return toFixed(float64(level*40)-30, 1)
|
||||||
|
case level >= 0.25:
|
||||||
|
return toFixed(float64(level*80)-50, 1)
|
||||||
|
case level >= 0.0625:
|
||||||
|
return toFixed(float64(level*160)-70, 1)
|
||||||
|
case level >= 0:
|
||||||
|
return toFixed(float64(level*480)-90, 1)
|
||||||
|
default:
|
||||||
|
return -90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFixed(num float64, precision int) float64 {
|
||||||
|
output := math.Pow(10, float64(precision))
|
||||||
|
return float64(math.Round(num*output)) / output
|
||||||
|
}
|
||||||
40
makefile
Normal file
40
makefile
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
program = xair-cli
|
||||||
|
|
||||||
|
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)" .
|
||||||
|
|
||||||
|
$(LINUX):
|
||||||
|
env GOOS=linux GOARCH=amd64 go build -v -o $(LINUX) -ldflags="-s -w -X main.version=$(VERSION)" .
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(GO) test ./...
|
||||||
|
|
||||||
|
$(BIN_DIR):
|
||||||
|
@mkdir -p $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rv $(BIN_DIR)
|
||||||
Loading…
x
Reference in New Issue
Block a user