4 Commits

Author SHA1 Message Date
9898c21197 add headamp gain and phantom commands 2026-02-01 01:25:47 +00:00
64b4be032f use addressMap in strip struct 2026-02-01 00:53:32 +00:00
d894cc1317 add strip/bus name commands 2026-02-01 00:50:24 +00:00
4c4d52c74e add --version flag 2026-01-31 23:13:09 +00:00
10 changed files with 410 additions and 48 deletions

View File

@@ -223,6 +223,50 @@ var busFadeInCmd = &cobra.Command{
},
}
// busNameCmd represents the bus name command.
var busNameCmd = &cobra.Command{
Short: "Get or set the bus name",
Long: `Get or set the name of a specific bus.`,
Use: "name [bus number] [new name]",
Example: ` # Get the name of bus 1
xair-cli bus name 1
# Set the name of bus 1 to "Vocals"
xair-cli bus name 1 Vocals`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
cmd.PrintErrln("OSC client not found in context")
return
}
if len(args) < 1 {
cmd.PrintErrln("Please provide bus number")
return
}
busIndex := mustConvToInt(args[0])
if len(args) == 1 {
name, err := client.Bus.Name(busIndex)
if err != nil {
cmd.PrintErrln("Error getting bus name:", err)
return
}
cmd.Printf("Bus %d name: %s\n", busIndex, name)
return
}
newName := args[1]
err := client.Bus.SetName(busIndex, newName)
if err != nil {
cmd.PrintErrln("Error setting bus name:", err)
return
}
cmd.Printf("Bus %d name set to: %s\n", busIndex, newName)
},
}
func init() {
rootCmd.AddCommand(busCmd)
@@ -233,4 +277,6 @@ func init() {
busFadeOutCmd.Flags().Float64P("duration", "d", 5.0, "Duration for fade out in seconds")
busCmd.AddCommand(busFadeInCmd)
busFadeInCmd.Flags().Float64P("duration", "d", 5.0, "Duration for fade in in seconds")
busCmd.AddCommand(busNameCmd)
}

146
cmd/headamp.go Normal file
View File

@@ -0,0 +1,146 @@
package cmd
import (
"github.com/spf13/cobra"
)
// headampCmd represents the headamp command
var headampCmd = &cobra.Command{
Short: "Commands to control headamp gain and phantom power",
Long: `Commands to control the headamp gain and phantom power settings of the XAir mixer.
You can get or set the gain level for individual headamps, as well as enable or disable phantom power.`,
Use: "headamp",
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}
// headampGainCmd represents the headamp gain command
var headampGainCmd = &cobra.Command{
Use: "gain",
Short: "Get or set headamp gain level",
Long: `Get or set the gain level for a specified headamp index.
Examples:
# Get gain level for headamp index 1
xairctl headamp gain 1
# Set gain level for headamp index 1 to 3.5 dB
xairctl headamp gain 1 3.5`,
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
cmd.PrintErrln("OSC client not found in context")
return
}
if len(args) < 1 {
cmd.PrintErrln("Please provide a headamp index")
return
}
index := mustConvToInt(args[0])
if len(args) == 1 {
gain, err := client.HeadAmp.Gain(index)
if err != nil {
cmd.PrintErrln("Error getting headamp gain level:", err)
return
}
cmd.Printf("Headamp %d Gain: %.2f dB\n", index, gain)
return
}
if len(args) < 2 {
cmd.PrintErrln("Please provide a gain level in dB")
return
}
level := mustConvToFloat64(args[1])
err := client.HeadAmp.SetGain(index, level)
if err != nil {
cmd.PrintErrln("Error setting headamp gain level:", err)
return
}
cmd.Printf("Headamp %d Gain set to %.2f dB\n", index, level)
},
}
// headampPhantomPowerCmd represents the headamp phantom power command
var headampPhantomPowerCmd = &cobra.Command{
Use: "phantom",
Short: "Get or set headamp phantom power status",
Long: `Get or set the phantom power status for a specified headamp index.
Examples:
# Get phantom power status for headamp index 1
xairctl headamp phantom 1
# Enable phantom power for headamp index 1
xairctl headamp phantom 1 on
# Disable phantom power for headamp index 1
xairctl headamp phantom 1 off`,
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
cmd.PrintErrln("OSC client not found in context")
return
}
if len(args) < 1 {
cmd.PrintErrln("Please provide a headamp index")
return
}
index := mustConvToInt(args[0])
if len(args) == 1 {
enabled, err := client.HeadAmp.PhantomPower(index)
if err != nil {
cmd.PrintErrln("Error getting headamp phantom power status:", err)
return
}
status := "disabled"
if enabled {
status = "enabled"
}
cmd.Printf("Headamp %d Phantom Power is %s\n", index, status)
return
}
if len(args) < 2 {
cmd.PrintErrln("Please provide phantom power status: on or off")
return
}
var enable bool
switch args[1] {
case "on", "enable":
enable = true
case "off", "disable":
enable = false
default:
cmd.PrintErrln("Invalid phantom power status. Use 'on' or 'off'")
return
}
err := client.HeadAmp.SetPhantomPower(index, enable)
if err != nil {
cmd.PrintErrln("Error setting headamp phantom power status:", err)
return
}
status := "disabled"
if enable {
status = "enabled"
}
cmd.Printf("Headamp %d Phantom Power %s successfully\n", index, status)
},
}
func init() {
rootCmd.AddCommand(headampCmd)
headampCmd.AddCommand(headampGainCmd)
headampCmd.AddCommand(headampPhantomPowerCmd)
}

View File

@@ -2,6 +2,7 @@ package cmd
import (
"os"
"runtime/debug"
"strings"
"github.com/charmbracelet/log"
@@ -11,6 +12,8 @@ import (
"github.com/onyx-and-iris/xair-cli/internal/xair"
)
var version string // Version of the CLI, set during build time
// rootCmd represents the base command when called without any subcommands.
var rootCmd = &cobra.Command{
Use: "xair-cli",
@@ -18,6 +21,7 @@ var rootCmd = &cobra.Command{
Long: `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.`,
Version: versionFromBuild(),
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
level, err := log.ParseLevel(viper.GetString("loglevel"))
if err != nil {
@@ -88,3 +92,15 @@ func init() {
viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
viper.BindPFlag("kind", rootCmd.PersistentFlags().Lookup("kind"))
}
func versionFromBuild() string {
if version == "" {
info, ok := debug.ReadBuildInfo()
if !ok {
return "(unable to read version)"
}
version = strings.Split(info.Main.Version, "-")[0]
}
return version
}

View File

@@ -305,6 +305,54 @@ var stripSendCmd = &cobra.Command{
},
}
// stripNameCmd represents the strip name command.
var stripNameCmd = &cobra.Command{
Short: "Get or set the name of a strip",
Long: `Get or set the name of a specific strip.
If no name argument is provided, the current strip name is retrieved.
If a name argument is provided, the strip name is set to that value.`,
Use: "name [strip number] [name]",
Example: ` # Get the current name of strip 1
xair-cli strip name 1
# Set the name of strip 1 to "Guitar"
xair-cli strip name 1 "Guitar"`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
cmd.PrintErrln("OSC client not found in context")
return
}
if len(args) < 1 {
cmd.PrintErrln("Please provide a strip number")
return
}
stripIndex := mustConvToInt(args[0])
if len(args) == 1 {
name, err := client.Strip.Name(stripIndex)
if err != nil {
cmd.PrintErrln("Error getting strip name:", err)
return
}
cmd.Printf("Strip %d name: %s\n", stripIndex, name)
return
}
name := args[1]
err := client.Strip.SetName(stripIndex, name)
if err != nil {
cmd.PrintErrln("Error setting strip name:", err)
return
}
cmd.Printf("Strip %d name set to: %s\n", stripIndex, name)
},
}
func init() {
rootCmd.AddCommand(stripCmd)
@@ -317,4 +365,6 @@ func init() {
stripFadeInCmd.Flags().Float64P("duration", "d", 5.0, "Duration of the fade in in seconds")
stripCmd.AddCommand(stripSendCmd)
stripCmd.AddCommand(stripNameCmd)
}

View File

@@ -1,11 +1,15 @@
package xair
var xairAddressMap = map[string]string{
"strip": "/ch/%02d",
"bus": "/bus/%01d",
"headamp": "/headamp/%02d",
}
var x32AddressMap = map[string]string{
"strip": "/ch/%02d",
"bus": "/bus/%02d",
"headamp": "/headamp/%02d",
}
func addressMapForMixerKind(kind MixerKind) map[string]string {

View File

@@ -3,11 +3,13 @@ package xair
import "fmt"
type Bus struct {
baseAddress string
client Client
}
func NewBus(c Client) *Bus {
return &Bus{
baseAddress: c.addressMap["bus"],
client: c,
}
}
@@ -31,8 +33,7 @@ func (b *Bus) Mute(bus int) (bool, error) {
// SetMute sets the mute status for a specific bus (1-based indexing)
func (b *Bus) SetMute(bus int, muted bool) error {
formatter := b.client.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/on"
address := fmt.Sprintf(b.baseAddress, bus) + "/mix/on"
var value int32
if !muted {
value = 1
@@ -42,8 +43,7 @@ func (b *Bus) SetMute(bus int, muted bool) error {
// Fader requests the current fader level for a bus
func (b *Bus) Fader(bus int) (float64, error) {
formatter := b.client.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/fader"
address := fmt.Sprintf(b.baseAddress, bus) + "/mix/fader"
err := b.client.SendMessage(address)
if err != nil {
return 0, err
@@ -60,7 +60,28 @@ func (b *Bus) Fader(bus int) (float64, error) {
// SetFader sets the fader level for a specific bus (1-based indexing)
func (b *Bus) SetFader(bus int, level float64) error {
formatter := b.client.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/fader"
address := fmt.Sprintf(b.baseAddress, bus) + "/mix/fader"
return b.client.SendMessage(address, float32(mustDbInto(level)))
}
// Name requests the name for a specific bus
func (b *Bus) Name(bus int) (string, error) {
address := fmt.Sprintf(b.baseAddress, bus) + "/config/name"
err := b.client.SendMessage(address)
if err != nil {
return "", fmt.Errorf("failed to send bus name request: %v", err)
}
resp := <-b.client.respChan
val, ok := resp.Arguments[0].(string)
if !ok {
return "", fmt.Errorf("unexpected argument type for bus name value")
}
return val, nil
}
// SetName sets the name for a specific bus
func (b *Bus) SetName(bus int, name string) error {
address := fmt.Sprintf(b.baseAddress, bus) + "/config/name"
return b.client.SendMessage(address, name)
}

View File

@@ -18,6 +18,7 @@ type Client struct {
Main *Main
Strip *Strip
Bus *Bus
HeadAmp *HeadAmp
}
// NewClient creates a new XAirClient instance
@@ -60,6 +61,7 @@ func NewClient(mixerIP string, mixerPort int, opts ...Option) (*Client, error) {
c.Main = newMain(*c)
c.Strip = NewStrip(*c)
c.Bus = NewBus(*c)
c.HeadAmp = NewHeadAmp(*c)
return c, nil
}

67
internal/xair/headamp.go Normal file
View File

@@ -0,0 +1,67 @@
package xair
import "fmt"
type HeadAmp struct {
baseAddress string
client Client
}
func NewHeadAmp(c Client) *HeadAmp {
return &HeadAmp{
baseAddress: c.addressMap["headamp"],
client: c,
}
}
// Gain gets the gain level for the specified headamp index.
func (h *HeadAmp) Gain(index int) (float64, error) {
address := fmt.Sprintf(h.baseAddress, index) + "/gain"
err := h.client.SendMessage(address)
if err != nil {
return 0, err
}
resp := <-h.client.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for headamp gain value")
}
return linGet(-12, 60, float64(val)), nil
}
// SetGain sets the gain level for the specified headamp index.
func (h *HeadAmp) SetGain(index int, level float64) error {
address := fmt.Sprintf(h.baseAddress, index) + "/gain"
return h.client.SendMessage(address, float32(linSet(-12, 60, level)))
}
// PhantomPower gets the phantom power status for the specified headamp index.
func (h *HeadAmp) PhantomPower(index int) (bool, error) {
address := fmt.Sprintf(h.baseAddress, index) + "/phantom"
err := h.client.SendMessage(address)
if err != nil {
return false, err
}
resp := <-h.client.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return false, fmt.Errorf("unexpected argument type for phantom power value")
}
return val != 0, nil
}
// SetPhantomPower sets the phantom power status for the specified headamp index.
func (h *HeadAmp) SetPhantomPower(index int, enabled bool) error {
address := fmt.Sprintf(h.baseAddress, index) + "/phantom"
var val int32
if enabled {
val = 1
} else {
val = 0
}
return h.client.SendMessage(address, val)
}

View File

@@ -3,18 +3,20 @@ package xair
import "fmt"
type Strip struct {
baseAddress string
client Client
}
func NewStrip(c Client) *Strip {
return &Strip{
baseAddress: c.addressMap["strip"],
client: c,
}
}
// Mute gets the mute status of the specified strip (1-based indexing).
func (s *Strip) Mute(strip int) (bool, error) {
address := fmt.Sprintf("/ch/%02d/mix/on", strip)
func (s *Strip) Mute(index int) (bool, error) {
address := fmt.Sprintf(s.baseAddress, index) + "/mix/on"
err := s.client.SendMessage(address)
if err != nil {
return false, err
@@ -30,7 +32,7 @@ func (s *Strip) Mute(strip int) (bool, error) {
// SetMute sets the mute status of the specified strip (1-based indexing).
func (s *Strip) SetMute(strip int, muted bool) error {
address := fmt.Sprintf("/ch/%02d/mix/on", strip)
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/on"
var value int32 = 0
if !muted {
value = 1
@@ -40,7 +42,7 @@ func (s *Strip) SetMute(strip int, muted bool) error {
// Fader gets the fader level of the specified strip (1-based indexing).
func (s *Strip) Fader(strip int) (float64, error) {
address := fmt.Sprintf("/ch/%02d/mix/fader", strip)
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/fader"
err := s.client.SendMessage(address)
if err != nil {
return 0, err
@@ -57,35 +59,13 @@ func (s *Strip) Fader(strip int) (float64, error) {
// SetFader sets the fader level of the specified strip (1-based indexing).
func (s *Strip) SetFader(strip int, level float64) error {
address := fmt.Sprintf("/ch/%02d/mix/fader", strip)
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/fader"
return s.client.SendMessage(address, float32(mustDbInto(level)))
}
// MicGain requests the phantom gain for a specific strip (1-based indexing).
func (s *Strip) MicGain(strip int) (float64, error) {
address := fmt.Sprintf("/ch/%02d/mix/gain", strip)
err := s.client.SendMessage(address)
if err != nil {
return 0, fmt.Errorf("failed to send strip gain request: %v", err)
}
resp := <-s.client.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for strip gain value")
}
return mustDbFrom(float64(val)), nil
}
// SetMicGain sets the phantom gain for a specific strip (1-based indexing).
func (s *Strip) SetMicGain(strip int, gain float32) error {
address := fmt.Sprintf("/ch/%02d/mix/gain", strip)
return s.client.SendMessage(address, gain)
}
// Name requests the name for a specific strip
func (s *Strip) Name(strip int) (string, error) {
address := fmt.Sprintf("/ch/%02d/config/name", strip)
address := fmt.Sprintf(s.baseAddress, strip) + "/config/name"
err := s.client.SendMessage(address)
if err != nil {
return "", fmt.Errorf("failed to send strip name request: %v", err)
@@ -101,13 +81,13 @@ func (s *Strip) Name(strip int) (string, error) {
// SetName sets the name for a specific strip
func (s *Strip) SetName(strip int, name string) error {
address := fmt.Sprintf("/ch/%02d/config/name", strip)
address := fmt.Sprintf(s.baseAddress, strip) + "/config/name"
return s.client.SendMessage(address, name)
}
// Color requests the color for a specific strip
func (s *Strip) Color(strip int) (int32, error) {
address := fmt.Sprintf("/ch/%02d/config/color", strip)
address := fmt.Sprintf(s.baseAddress, strip) + "/config/color"
err := s.client.SendMessage(address)
if err != nil {
return 0, fmt.Errorf("failed to send strip color request: %v", err)
@@ -123,13 +103,13 @@ func (s *Strip) Color(strip int) (int32, error) {
// SetColor sets the color for a specific strip (0-15)
func (s *Strip) SetColor(strip int, color int32) error {
address := fmt.Sprintf("/ch/%02d/config/color", strip)
address := fmt.Sprintf(s.baseAddress, strip) + "/config/color"
return s.client.SendMessage(address, color)
}
// Sends requests the sends level for a mixbus.
func (s *Strip) SendLevel(strip int, bus int) (float64, error) {
address := fmt.Sprintf("/ch/%02d/mix/%02d/level", strip, bus)
address := fmt.Sprintf(s.baseAddress, strip) + fmt.Sprintf("/mix/%02d/level", bus)
err := s.client.SendMessage(address)
if err != nil {
return 0, fmt.Errorf("failed to send strip send level request: %v", err)
@@ -145,6 +125,28 @@ func (s *Strip) SendLevel(strip int, bus int) (float64, error) {
// SetSendLevel sets the sends level for a mixbus.
func (s *Strip) SetSendLevel(strip int, bus int, level float64) error {
address := fmt.Sprintf("/ch/%02d/mix/%02d/level", strip, bus)
address := fmt.Sprintf(s.baseAddress, strip) + fmt.Sprintf("/mix/%02d/level", bus)
return s.client.SendMessage(address, float32(mustDbInto(level)))
}
// MicGain requests the phantom gain for a specific strip (1-based indexing).
func (s *Strip) MicGain(strip int) (float64, error) {
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/gain"
err := s.client.SendMessage(address)
if err != nil {
return 0, fmt.Errorf("failed to send strip gain request: %v", err)
}
resp := <-s.client.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for strip gain value")
}
return mustDbFrom(float64(val)), nil
}
// SetMicGain sets the phantom gain for a specific strip (1-based indexing).
func (s *Strip) SetMicGain(strip int, gain float64) error {
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/gain"
return s.client.SendMessage(address, float32(mustDbInto(gain)))
}

View File

@@ -2,6 +2,14 @@ package xair
import "math"
func linGet(min float64, max float64, value float64) float64 {
return min + (max-min)*value
}
func linSet(min float64, max float64, value float64) float64 {
return (value - min) / (max - min)
}
func mustDbInto(db float64) float64 {
switch {
case db >= 10: