16 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
7536c4fe24 reword short desc for strip send 2026-01-31 22:59:26 +00:00
2730f8dc5d reorganise command fields 2026-01-31 22:26:31 +00:00
e7dd589243 update use strings 2026-01-31 21:33:05 +00:00
a0663350f8 add strip send command 2026-01-31 21:30:48 +00:00
ad7c910180 move main methods into Main struct
update the cli
2026-01-31 21:19:38 +00:00
615a95d9da move bus commands into bus struct
update the cli
2026-01-31 20:55:36 +00:00
19779ae4c1 move lower level logic into engine 2026-01-31 20:48:09 +00:00
fc8c8ad69a move strip commands into their own struct
update the cli
2026-01-31 20:40:29 +00:00
205baf310f lint fix 2026-01-31 20:04:31 +00:00
d823aeeb8e lint fixes 2026-01-31 19:33:04 +00:00
d1657e09ab implement strip fader commands 2026-01-31 19:31:33 +00:00
c851d0e804 lint fixes 2026-01-31 19:28:59 +00:00
17 changed files with 1082 additions and 443 deletions

View File

@@ -1,30 +1,26 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
)
// busCmd represents the bus command
// busCmd represents the bus command.
var busCmd = &cobra.Command{
Use: "bus",
Short: "Commands to control individual buses",
Long: `Commands to control individual buses of the XAir mixer, including mute status.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("bus called")
Use: "bus",
Run: func(cmd *cobra.Command, _ []string) {
cmd.Help()
},
}
// busMuteCmd represents the bus mute command
// busMuteCmd represents the bus mute command.
var busMuteCmd = &cobra.Command{
Use: "mute",
Short: "Get or set the bus mute status",
Long: `Get or set the mute status of a specific bus.`,
Use: "mute [bus number] [true|false]",
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -49,7 +45,7 @@ var busMuteCmd = &cobra.Command{
return
}
err := client.SetBusMute(busNum, muted)
err := client.Bus.SetMute(busNum, muted)
if err != nil {
cmd.PrintErrln("Error setting bus mute status:", err)
return
@@ -58,11 +54,18 @@ var busMuteCmd = &cobra.Command{
},
}
// busFaderCmd represents the bus fader command
// busFaderCmd represents the bus fader command.
var busFaderCmd = &cobra.Command{
Use: "fader",
Short: "Get or set the bus fader level",
Long: `Get or set the fader level of a specific bus.`,
Long: `Get or set the fader level of a specific bus.
If no level argument is provided, the current fader level is retrieved.
If a level argument (in dB) is provided, the bus fader is set to that level.`,
Use: "fader [bus number] [level in dB]",
Example: ` # Get the current fader level of bus 1
xair-cli bus fader 1
# Set the fader level of bus 1 to -10.0 dB
xair-cli bus fader 1 -10.0`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -70,14 +73,15 @@ var busFaderCmd = &cobra.Command{
return
}
busIndex := mustConvToInt(args[0])
if len(args) == 1 {
busNum := mustConvToInt(args[0])
level, err := client.BusFader(busNum)
level, err := client.Bus.Fader(busIndex)
if err != nil {
cmd.PrintErrln("Error getting bus fader level:", err)
return
}
cmd.Printf("Bus %d fader level: %.1f dB\n", busNum, level)
cmd.Printf("Bus %d fader level: %.1f dB\n", busIndex, level)
return
}
@@ -86,28 +90,24 @@ var busFaderCmd = &cobra.Command{
return
}
busNum := mustConvToInt(args[0])
level := mustConvToFloat64(args[1])
err := client.SetBusFader(busNum, level)
err := client.Bus.SetFader(busIndex, level)
if err != nil {
cmd.PrintErrln("Error setting bus fader level:", err)
return
}
cmd.Printf("Bus %d fader set to %.2f dB\n", busNum, level)
cmd.Printf("Bus %d fader set to %.2f dB\n", busIndex, level)
},
}
// busFadeOutCmd represents the bus fade out command
// busFadeOutCmd represents the bus fade out command.
var busFadeOutCmd = &cobra.Command{
Use: "fadeout",
Short: "Fade out the bus fader over a specified duration",
Long: `Fade out the bus fader to minimum level over a specified duration in seconds.
For example:
# Fade out bus 1 over 5 seconds
xair-cli bus fadeout 1 --duration 5
`,
Long: "Fade out the bus fader to minimum level over a specified duration in seconds.",
Use: "fadeout [bus number] --duration [seconds] [target level in dB]",
Example: ` # Fade out bus 1 over 5 seconds
xair-cli bus fadeout 1 --duration 5 -- -90.0`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -133,7 +133,7 @@ For example:
target = mustConvToFloat64(args[1])
}
currentFader, err := client.BusFader(busIndex)
currentFader, err := client.Bus.Fader(busIndex)
if err != nil {
cmd.PrintErrln("Error getting current bus fader level:", err)
return
@@ -142,7 +142,7 @@ For example:
// Calculate total steps needed to reach target dB
totalSteps := float64(currentFader - target)
if totalSteps <= 0 {
cmd.Println("Bus is already faded out")
cmd.Println("Bus is already at or below target level")
return
}
@@ -150,7 +150,7 @@ For example:
for currentFader > target {
currentFader -= 1.0
err := client.SetBusFader(busIndex, currentFader)
err := client.Bus.SetFader(busIndex, currentFader)
if err != nil {
cmd.PrintErrln("Error setting bus fader level:", err)
return
@@ -162,16 +162,13 @@ For example:
},
}
// BusFadeInCmd represents the bus fade in command
// BusFadeInCmd represents the bus fade in command.
var busFadeInCmd = &cobra.Command{
Use: "fadein",
Short: "Fade in the bus fader over a specified duration",
Long: `Fade in the bus fader to maximum level over a specified duration in seconds.
For example:
# Fade in bus 1 over 5 seconds
xair-cli bus fadein 1 --duration 5
`,
Long: "Fade in the bus fader to maximum level over a specified duration in seconds.",
Use: "fadein [bus number] --duration [seconds] [target level in dB]",
Example: ` # Fade in bus 1 over 5 seconds
xair-cli bus fadein 1 --duration 5 -- 0.0`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -197,7 +194,7 @@ For example:
target = mustConvToFloat64(args[1])
}
currentFader, err := client.BusFader(busIndex)
currentFader, err := client.Bus.Fader(busIndex)
if err != nil {
cmd.PrintErrln("Error getting current bus fader level:", err)
return
@@ -214,7 +211,7 @@ For example:
for currentFader < target {
currentFader += 1.0
err := client.SetBusFader(busIndex, currentFader)
err := client.Bus.SetFader(busIndex, currentFader)
if err != nil {
cmd.PrintErrln("Error setting bus fader level:", err)
return
@@ -226,6 +223,50 @@ For example:
},
}
// 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,7 +274,9 @@ func init() {
busCmd.AddCommand(busFaderCmd)
busCmd.AddCommand(busFadeOutCmd)
busFadeOutCmd.Flags().Float64P("duration", "d", 5, "Duration for fade out in seconds")
busFadeOutCmd.Flags().Float64P("duration", "d", 5.0, "Duration for fade out in seconds")
busCmd.AddCommand(busFadeInCmd)
busFadeInCmd.Flags().Float64P("duration", "d", 5, "Duration for fade in in seconds")
busFadeInCmd.Flags().Float64P("duration", "d", 5.0, "Duration for fade in in seconds")
busCmd.AddCommand(busNameCmd)
}

View File

@@ -1,6 +1,3 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package cmd
import (
@@ -11,10 +8,12 @@ import (
type clientKey string
// WithContext returns a new context with the provided xair.Client.
func WithContext(ctx context.Context, client *xair.Client) context.Context {
return context.WithValue(ctx, clientKey("oscClient"), client)
}
// ClientFromContext retrieves the xair.Client from the context.
func ClientFromContext(ctx context.Context) *xair.Client {
if client, ok := ctx.Value(clientKey("oscClient")).(*xair.Client); ok {
return client

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

@@ -1,6 +1,3 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package cmd
import (
@@ -9,35 +6,33 @@ import (
"github.com/spf13/cobra"
)
// mainCmd represents the main command
// mainCmd represents the main command.
var mainCmd = &cobra.Command{
Use: "main",
Short: "Commands to control the main output",
Long: `Commands to control the main output of the XAir mixer, including fader level and mute status.`,
Run: func(cmd *cobra.Command, args []string) {
Use: "main",
Run: func(cmd *cobra.Command, _ []string) {
cmd.Help()
},
}
// mainMuteCmd represents the main mute command.
var mainMuteCmd = &cobra.Command{
Use: "mute",
Short: "Get or set the main LR mute status",
Long: `Get or set the main L/R mute status.
If no argument is provided, the current mute status is retrieved.
If "true" or "1" is provided as an argument, the main output is muted.
If "false" or "0" is provided, the main output is unmuted.
For example:
# Get the current main LR mute status
If "false" or "0" is provided, the main output is unmuted.`,
Use: "mute [true|false]",
Example: ` # Get the current main LR mute status
xair-cli main mute
# Mute the main output
xair-cli main mute true
# Unmute the main output
xair-cli main mute false
`,
xair-cli main mute false`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -46,7 +41,7 @@ For example:
}
if len(args) == 0 {
resp, err := client.MainLRMute()
resp, err := client.Main.Mute()
if err != nil {
cmd.PrintErrln("Error getting main LR mute status:", err)
return
@@ -56,11 +51,17 @@ For example:
}
var muted bool
if args[0] == "true" || args[0] == "1" {
switch args[0] {
case "true", "1":
muted = true
case "false", "0":
muted = false
default:
cmd.PrintErrln("Invalid mute status. Use true/false or 1/0")
return
}
err := client.SetMainLRMute(muted)
err := client.Main.SetMute(muted)
if err != nil {
cmd.PrintErrln("Error setting main LR mute status:", err)
return
@@ -69,21 +70,19 @@ For example:
},
}
// mainFaderCmd represents the main fader command.
var mainFaderCmd = &cobra.Command{
Use: "fader",
Short: "Set or get the main LR fader level",
Long: `Set or get the main L/R fader level in dB.
If no argument is provided, the current fader level is retrieved.
If a dB value is provided as an argument, the fader level is set to that value.
For example:
# Get the current main LR fader level
If a dB value is provided as an argument, the fader level is set to that value.`,
Use: "fader [level in dB]",
Example: ` # Get the current main LR fader level
xair-cli main fader
# Set the main LR fader level to -10.0 dB
xair-cli main fader -- -10.0
`,
xair-cli main fader -- -10.0`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -92,7 +91,7 @@ For example:
}
if len(args) == 0 {
resp, err := client.MainLRFader()
resp, err := client.Main.Fader()
if err != nil {
cmd.PrintErrln("Error getting main LR fader:", err)
return
@@ -101,7 +100,7 @@ For example:
return
}
err := client.SetMainLRFader(mustConvToFloat64(args[0]))
err := client.Main.SetFader(mustConvToFloat64(args[0]))
if err != nil {
cmd.PrintErrln("Error setting main LR fader:", err)
return
@@ -110,17 +109,16 @@ For example:
},
}
// mainFadeOutCmd represents the main fadeout command.
var mainFadeOutCmd = &cobra.Command{
Use: "fadeout [target_db]",
Short: "Fade out the main output",
Long: `Fade out the main output over a specified duration.
For example:
xair-cli main fadeout --duration 10 -- -20.0
xair-cli main fadeout -- -90.0 # Uses default 5 second duration
This command will fade out the main output to the specified dB level.
`,
Use: "fadeout --duration [seconds] [target_db]",
Example: ` # Fade out main output over 5 seconds
xair-cli main fadeout --duration 5 -- -90.0`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -140,7 +138,7 @@ This command will fade out the main output to the specified dB level.
target = mustConvToFloat64(args[0])
}
currentFader, err := client.MainLRFader()
currentFader, err := client.Main.Fader()
if err != nil {
cmd.PrintErrln("Error getting current main LR fader:", err)
return
@@ -158,7 +156,7 @@ This command will fade out the main output to the specified dB level.
for currentFader > target {
currentFader -= 1.0
err = client.SetMainLRFader(currentFader)
err = client.Main.SetFader(currentFader)
if err != nil {
cmd.PrintErrln("Error setting main LR fader:", err)
return
@@ -169,18 +167,16 @@ This command will fade out the main output to the specified dB level.
},
}
// mainFadeInCmd represents the main fadein command.
var mainFadeInCmd = &cobra.Command{
Use: "fadein [target_db]",
Short: "Fade in the main output",
Long: `Fade in the main output over a specified duration.
For example:
xair-cli main fadein --duration 10 -- -6.0
xair-cli main fadein -- -0.0 # Uses default 5 second duration
This command will fade in the main output to the specified dB level.
`,
Use: "fadein --duration [seconds] [target_db]",
Example: ` # Fade in main output over 5 seconds
xair-cli main fadein --duration 5 -- 0.0`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -199,7 +195,7 @@ This command will fade in the main output to the specified dB level.
target = mustConvToFloat64(args[0])
}
currentFader, err := client.MainLRFader()
currentFader, err := client.Main.Fader()
if err != nil {
cmd.PrintErrln("Error getting current main LR fader:", err)
return
@@ -217,7 +213,7 @@ This command will fade in the main output to the specified dB level.
for currentFader < target {
currentFader += 1.0
err = client.SetMainLRFader(currentFader)
err = client.Main.SetFader(currentFader)
if err != nil {
cmd.PrintErrln("Error setting main LR fader:", err)
return

View File

@@ -1,10 +1,8 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package cmd
import (
"os"
"runtime/debug"
"strings"
"github.com/charmbracelet/log"
@@ -14,14 +12,17 @@ import (
"github.com/onyx-and-iris/xair-cli/internal/xair"
)
// rootCmd represents the base command when called without any subcommands
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",
Short: "A command-line utility to interact with Behringer X Air mixers via OSC",
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.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
Version: versionFromBuild(),
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
level, err := log.ParseLevel(viper.GetString("loglevel"))
if err != nil {
return err
@@ -29,7 +30,7 @@ various commands to manage mixer settings directly from the terminal.`,
log.SetLevel(level)
kind := viper.GetString("kind")
log.Debugf("Initializing client for mixer kind: %s", kind)
log.Debugf("Initialising client for mixer kind: %s", kind)
if kind == "x32" && !viper.IsSet("port") {
viper.Set("port", 10023)
@@ -55,18 +56,20 @@ various commands to manage mixer settings directly from the terminal.`,
return nil
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
PersistentPostRunE: func(cmd *cobra.Command, _ []string) error {
client := ClientFromContext(cmd.Context())
if client != nil {
client.Stop()
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, _ []string) {
cmd.Help()
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
@@ -89,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

@@ -1,42 +1,37 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
)
// stripCmd represents the strip command
// stripCmd represents the strip command.
var stripCmd = &cobra.Command{
Use: "strip",
Short: "Commands to control individual strips",
Long: `Commands to control individual strips of the XAir mixer, including fader level and mute status.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("strip called")
Use: "strip",
Run: func(cmd *cobra.Command, _ []string) {
cmd.Help()
},
}
// stripMuteCmd represents the strip mute command.
var stripMuteCmd = &cobra.Command{
Use: "mute",
Short: "Get or set the mute status of a strip",
Long: `Get or set the mute status of a specific strip.
If no argument is provided, the current mute status is retrieved.
If "true" or "1" is provided as an argument, the strip is muted.
If "false" or "0" is provided, the strip is unmuted.
For example:
# Get the current mute status of strip 1
If "false" or "0" is provided, the strip is unmuted.`,
Use: "mute [strip number] [true|false]",
Example: ` # Get the current mute status of strip 1
xair-cli strip mute 1
# Mute strip 1
xair-cli strip mute 1 true
# Unmute strip 1
xair-cli strip mute 1 false
`,
xair-cli strip mute 1 false`,
Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context())
if client == nil {
@@ -52,7 +47,7 @@ For example:
stripIndex := mustConvToInt(args[0])
if len(args) == 1 {
resp, err := client.StripMute(stripIndex)
resp, err := client.Strip.Mute(stripIndex)
if err != nil {
cmd.PrintErrln("Error getting strip mute status:", err)
return
@@ -72,7 +67,7 @@ For example:
return
}
err := client.SetStripMute(stripIndex, muted)
err := client.Strip.SetMute(stripIndex, muted)
if err != nil {
cmd.PrintErrln("Error setting strip mute status:", err)
return
@@ -85,8 +80,291 @@ For example:
},
}
// stripFaderCmd represents the strip fader command.
var stripFaderCmd = &cobra.Command{
Short: "Get or set the fader level of a strip",
Long: `Get or set the fader level of a specific strip.
If no level argument is provided, the current fader level is retrieved.
If a level argument (in dB) is provided, the strip fader is set to that level.`,
Use: "fader [strip number] [level in dB]",
Example: ` # Get the current fader level of strip 1
xair-cli strip fader 1
# Set the fader level of strip 1 to -10.0 dB
xair-cli strip fader 1 -10.0`,
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 {
level, err := client.Strip.Fader(stripIndex)
if err != nil {
cmd.PrintErrln("Error getting strip fader level:", err)
return
}
cmd.Printf("Strip %d fader level: %.2f\n", stripIndex, level)
return
}
if len(args) < 2 {
cmd.PrintErrln("Please provide a fader level in dB")
return
}
level := mustConvToFloat64(args[1])
err := client.Strip.SetFader(stripIndex, level)
if err != nil {
cmd.PrintErrln("Error setting strip fader level:", err)
return
}
cmd.Printf("Strip %d fader set to %.2f dB\n", stripIndex, level)
},
}
// stripFadeOutCmd represents the strip fade out command.
var stripFadeOutCmd = &cobra.Command{
Short: "Fade out the strip over a specified duration",
Long: "Fade out the strip over a specified duration in seconds.",
Use: "fadeout [strip number] --duration [seconds] [target level in dB]",
Example: ` # Fade out strip 1 over 5 seconds
xair-cli strip fadeout 1 --duration 5.0 -- -90.0`,
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 strip number")
return
}
stripIndex := mustConvToInt(args[0])
duration, err := cmd.Flags().GetFloat64("duration")
if err != nil {
cmd.PrintErrln("Error getting duration flag:", err)
return
}
target := -90.0
if len(args) > 1 {
target = mustConvToFloat64(args[1])
}
currentFader, err := client.Strip.Fader(stripIndex)
if err != nil {
cmd.PrintErrln("Error getting current strip fader level:", err)
return
}
totalSteps := float64(currentFader - target)
if totalSteps <= 0 {
cmd.Println("Strip is already at or below target level")
return
}
stepDelay := time.Duration(duration*1000/totalSteps) * time.Millisecond
for currentFader > target {
currentFader -= 1.0
err := client.Strip.SetFader(stripIndex, currentFader)
if err != nil {
cmd.PrintErrln("Error setting strip fader level:", err)
return
}
time.Sleep(stepDelay)
}
cmd.Printf("Strip %d faded out to %.2f dB over %.2f seconds\n", stripIndex, target, duration)
},
}
// stripFadeInCmd represents the strip fade in command.
var stripFadeInCmd = &cobra.Command{
Short: "Fade in the strip over a specified duration",
Long: "Fade in the strip over a specified duration in seconds.",
Use: "fadein [strip number] --duration [seconds] [target level in dB]",
Example: ` # Fade in strip 1 over 5 seconds
xair-cli strip fadein 1 --duration 5.0 0`,
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 strip number")
return
}
stripIndex := mustConvToInt(args[0])
duration, err := cmd.Flags().GetFloat64("duration")
if err != nil {
cmd.PrintErrln("Error getting duration flag:", err)
return
}
target := 0.0
if len(args) > 1 {
target = mustConvToFloat64(args[1])
}
currentFader, err := client.Strip.Fader(stripIndex)
if err != nil {
cmd.PrintErrln("Error getting current strip fader level:", err)
return
}
totalSteps := float64(target - currentFader)
if totalSteps <= 0 {
cmd.Println("Strip is already at or above target level")
return
}
stepDelay := time.Duration(duration*1000/totalSteps) * time.Millisecond
for currentFader < target {
currentFader += 1.0
err := client.Strip.SetFader(stripIndex, currentFader)
if err != nil {
cmd.PrintErrln("Error setting strip fader level:", err)
return
}
time.Sleep(stepDelay)
}
cmd.Printf("Strip %d faded in to %.2f dB over %.2f seconds\n", stripIndex, target, duration)
},
}
// stripSendCmd represents the strip send command.
var stripSendCmd = &cobra.Command{
Short: "Get or set the send levels for individual strips",
Long: "Get or set the send level from a specific strip to a specific bus.",
Use: "send [strip number] [bus number] [level in dB]",
Example: ` # Get the send level of strip 1 to bus 1
xair-cli strip send 1 1
# Set the send level of strip 1 to bus 1 to -5.0 dB
xair-cli strip send 1 1 -- -5.0`,
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) < 2 {
cmd.PrintErrln("Please provide strip number and bus number")
return
}
stripIndex, busIndex := func() (int, int) {
return mustConvToInt(args[0]), mustConvToInt(args[1])
}()
if len(args) == 2 {
currentLevel, err := client.Strip.SendLevel(stripIndex, busIndex)
if err != nil {
cmd.PrintErrln("Error getting strip send level:", err)
return
}
cmd.Printf("Strip %d send level to bus %d: %.2f dB\n", stripIndex, busIndex, currentLevel)
return
}
if len(args) < 3 {
cmd.PrintErrln("Please provide a send level in dB")
return
}
level := mustConvToFloat64(args[2])
err := client.Strip.SetSendLevel(stripIndex, busIndex, level)
if err != nil {
cmd.PrintErrln("Error setting strip send level:", err)
return
}
cmd.Printf("Strip %d send level to bus %d set to %.2f dB\n", stripIndex, busIndex, level)
},
}
// 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)
stripCmd.AddCommand(stripMuteCmd)
stripCmd.AddCommand(stripFaderCmd)
stripCmd.AddCommand(stripFadeOutCmd)
stripFadeOutCmd.Flags().Float64P("duration", "d", 5.0, "Duration of the fade out in seconds")
stripCmd.AddCommand(stripFadeInCmd)
stripFadeInCmd.Flags().Float64P("duration", "d", 5.0, "Duration of the fade in in seconds")
stripCmd.AddCommand(stripSendCmd)
stripCmd.AddCommand(stripNameCmd)
}

View File

@@ -1,12 +1,10 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package cmd
import (
"strconv"
)
// mustConvToFloat64 converts a string to float64, panicking on error.
func mustConvToFloat64(floatStr string) float64 {
level, err := strconv.ParseFloat(floatStr, 64)
if err != nil {
@@ -15,6 +13,7 @@ func mustConvToFloat64(floatStr string) float64 {
return level
}
// mustConvToInt converts a string to int, panicking on error.
func mustConvToInt(intStr string) int {
val, err := strconv.Atoi(intStr)
if err != nil {

View File

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

87
internal/xair/bus.go Normal file
View File

@@ -0,0 +1,87 @@
package xair
import "fmt"
type Bus struct {
baseAddress string
client Client
}
func NewBus(c Client) *Bus {
return &Bus{
baseAddress: c.addressMap["bus"],
client: c,
}
}
// Mute requests the current mute status for a bus
func (b *Bus) Mute(bus int) (bool, error) {
formatter := b.client.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/on"
err := b.client.SendMessage(address)
if err != nil {
return false, err
}
resp := <-b.client.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return false, fmt.Errorf("unexpected argument type for bus mute value")
}
return val == 0, nil
}
// SetMute sets the mute status for a specific bus (1-based indexing)
func (b *Bus) SetMute(bus int, muted bool) error {
address := fmt.Sprintf(b.baseAddress, bus) + "/mix/on"
var value int32
if !muted {
value = 1
}
return b.client.SendMessage(address, value)
}
// Fader requests the current fader level for a bus
func (b *Bus) Fader(bus int) (float64, error) {
address := fmt.Sprintf(b.baseAddress, bus) + "/mix/fader"
err := b.client.SendMessage(address)
if err != nil {
return 0, err
}
resp := <-b.client.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for bus fader value")
}
return mustDbFrom(float64(val)), nil
}
// SetFader sets the fader level for a specific bus (1-based indexing)
func (b *Bus) SetFader(bus int, level float64) error {
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

@@ -1,12 +1,8 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package xair
import (
"fmt"
"net"
"time"
"github.com/charmbracelet/log"
@@ -17,20 +13,12 @@ type parser interface {
Parse(data []byte) (*osc.Message, error)
}
type engine struct {
Kind MixerKind
conn *net.UDPConn
mixerAddr *net.UDPAddr
parser parser
addressMap map[string]string
done chan bool
respChan chan *osc.Message
}
type Client struct {
engine
Main *Main
Strip *Strip
Bus *Bus
HeadAmp *HeadAmp
}
// NewClient creates a new XAirClient instance
@@ -67,104 +55,34 @@ func NewClient(mixerIP string, mixerPort int, opts ...Option) (*Client, error) {
opt(e)
}
return &Client{
c := &Client{
engine: *e,
}, nil
}
c.Main = newMain(*c)
c.Strip = NewStrip(*c)
c.Bus = NewBus(*c)
c.HeadAmp = NewHeadAmp(*c)
return c, nil
}
// Start begins listening for messages in a goroutine
func (c *Client) StartListening() {
go c.receiveLoop()
log.Debugf("Started listening on %s...", c.conn.LocalAddr().String())
}
// receiveLoop handles incoming OSC messages
func (c *Client) receiveLoop() {
buffer := make([]byte, 4096)
for {
select {
case <-c.done:
return
default:
// Set read timeout to avoid blocking forever
c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
n, _, err := c.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 <-c.done:
return
default:
log.Errorf("Read error: %v", err)
return
}
}
msg, err := c.parseOSCMessage(buffer[:n])
if err != nil {
log.Errorf("Failed to parse OSC message: %v", err)
continue
}
c.respChan <- msg
}
}
}
// parseOSCMessage parses raw bytes into an OSC message with improved error handling
func (c *Client) parseOSCMessage(data []byte) (*osc.Message, error) {
msg, err := c.parser.Parse(data)
if err != nil {
return nil, err
}
return msg, nil
go c.engine.receiveLoop()
log.Debugf("Started listening on %s...", c.engine.conn.LocalAddr().String())
}
// Stop stops the client and closes the connection
func (c *Client) Stop() {
close(c.done)
if c.conn != nil {
c.conn.Close()
close(c.engine.done)
if c.engine.conn != nil {
c.engine.conn.Close()
}
}
// SendMessage sends an OSC message to the mixer using the unified connection
func (c *Client) SendMessage(address string, args ...any) error {
return c.SendToAddress(c.mixerAddr, address, args...)
}
// SendToAddress sends an OSC message to a specific address (enables replying to different ports)
func (c *Client) 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 = c.conn.WriteToUDP(data, addr)
return err
return c.engine.sendToAddress(c.mixerAddr, address, args...)
}
// RequestInfo requests mixer information
@@ -193,221 +111,3 @@ func (c *Client) KeepAlive() error {
func (c *Client) RequestStatus() error {
return c.SendMessage("/status")
}
/* STRIP METHODS */
// StripMute gets mute state for a specific strip (1-based indexing)
func (c *Client) StripMute(strip int) (bool, error) {
address := fmt.Sprintf("/ch/%02d/mix/on", strip)
err := c.SendMessage(address)
if err != nil {
return false, err
}
resp := <-c.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return false, fmt.Errorf("unexpected argument type for strip mute value")
}
return val == 0, nil
}
// SetStripMute sets mute state for a specific strip (1-based indexing)
func (c *Client) SetStripMute(strip int, muted bool) error {
address := fmt.Sprintf("/ch/%02d/mix/on", strip)
var value int32 = 0
if !muted {
value = 1
}
return c.SendMessage(address, value)
}
// StripFader requests the current fader level for a strip
func (c *Client) StripFader(strip int) (float64, error) {
address := fmt.Sprintf("/ch/%02d/mix/fader", strip)
err := c.SendMessage(address)
if err != nil {
return 0, err
}
resp := <-c.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for fader value")
}
return mustDbFrom(float64(val)), nil
}
// SetStripFader sets the fader level for a specific strip (1-based indexing)
func (c *Client) SetStripFader(strip int, level float64) error {
address := fmt.Sprintf("/ch/%02d/mix/fader", strip)
return c.SendMessage(address, float32(mustDbInto(level)))
}
// StripMicGain requests the phantom gain for a specific strip
func (c *Client) StripMicGain(strip int) (float64, error) {
address := fmt.Sprintf("/ch/%02d/mix/gain", strip)
err := c.SendMessage(address)
if err != nil {
return 0, fmt.Errorf("failed to send strip gain request: %v", err)
}
resp := <-c.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
}
// SetStripMicGain sets the phantom gain for a specific strip (1-based indexing)
func (c *Client) SetStripMicGain(strip int, gain float32) error {
address := fmt.Sprintf("/ch/%02d/mix/gain", strip)
return c.SendMessage(address, gain)
}
// StripName requests the name for a specific strip
func (c *Client) StripName(strip int) (string, error) {
address := fmt.Sprintf("/ch/%02d/config/name", strip)
err := c.SendMessage(address)
if err != nil {
return "", fmt.Errorf("failed to send strip name request: %v", err)
}
resp := <-c.respChan
val, ok := resp.Arguments[0].(string)
if !ok {
return "", fmt.Errorf("unexpected argument type for strip name value")
}
return val, nil
}
// SetStripName sets the name for a specific strip
func (c *Client) SetStripName(strip int, name string) error {
address := fmt.Sprintf("/ch/%02d/config/name", strip)
return c.SendMessage(address, name)
}
// StripColor requests the color for a specific strip
func (c *Client) StripColor(strip int) (int32, error) {
address := fmt.Sprintf("/ch/%02d/config/color", strip)
err := c.SendMessage(address)
if err != nil {
return 0, fmt.Errorf("failed to send strip color request: %v", err)
}
resp := <-c.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for strip color value")
}
return val, nil
}
// SetStripColor sets the color for a specific strip (0-15)
func (c *Client) SetStripColor(strip int, color int32) error {
address := fmt.Sprintf("/ch/%02d/config/color", strip)
return c.SendMessage(address, color)
}
/* BUS METHODS */
// BusMute requests the current mute status for a bus
func (c *Client) BusMute(bus int) (bool, error) {
formatter := c.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/on"
err := c.SendMessage(address)
if err != nil {
return false, err
}
resp := <-c.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return false, fmt.Errorf("unexpected argument type for bus mute value")
}
return val == 0, nil
}
// SetBusMute sets the mute status for a specific bus (1-based indexing)
func (c *Client) SetBusMute(bus int, muted bool) error {
formatter := c.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/on"
var value int32
if !muted {
value = 1
}
return c.SendMessage(address, value)
}
// BusFader requests the current fader level for a bus
func (c *Client) BusFader(bus int) (float64, error) {
formatter := c.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/fader"
err := c.SendMessage(address)
if err != nil {
return 0, err
}
resp := <-c.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for bus fader value")
}
return mustDbFrom(float64(val)), nil
}
// SetBusFader sets the fader level for a specific bus (1-based indexing)
func (c *Client) SetBusFader(bus int, level float64) error {
formatter := c.addressMap["bus"]
address := fmt.Sprintf(formatter, bus) + "/mix/fader"
return c.SendMessage(address, float32(mustDbInto(level)))
}
/* MAIN LR METHODS */
// MainLRFader requests the current main L/R fader level
func (c *Client) MainLRFader() (float64, error) {
err := c.SendMessage("/lr/mix/fader")
if err != nil {
return 0, err
}
resp := <-c.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 (c *Client) SetMainLRFader(level float64) error {
return c.SendMessage("/lr/mix/fader", float32(mustDbInto(level)))
}
// MainLRMute requests the current main L/R mute status
func (c *Client) MainLRMute() (bool, error) {
err := c.SendMessage("/lr/mix/on")
if err != nil {
return false, err
}
resp := <-c.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return false, fmt.Errorf("unexpected argument type for main LR mute value")
}
return val == 0, nil
}
// SetMainLRMute sets the main L/R mute status
func (c *Client) SetMainLRMute(muted bool) error {
var value int32
if !muted {
value = 1
}
return c.SendMessage("/lr/mix/on", value)
}

97
internal/xair/engine.go Normal file
View File

@@ -0,0 +1,97 @@
package xair
import (
"fmt"
"net"
"time"
"github.com/charmbracelet/log"
"github.com/hypebeast/go-osc/osc"
)
type engine struct {
Kind MixerKind
conn *net.UDPConn
mixerAddr *net.UDPAddr
parser parser
addressMap map[string]string
done chan bool
respChan chan *osc.Message
}
// receiveLoop handles incoming OSC messages
func (e *engine) receiveLoop() {
buffer := make([]byte, 4096)
for {
select {
case <-e.done:
return
default:
// Set read timeout to avoid blocking forever
e.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
n, _, err := e.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 <-e.done:
return
default:
log.Errorf("Read error: %v", err)
return
}
}
msg, err := e.parseOSCMessage(buffer[:n])
if err != nil {
log.Errorf("Failed to parse OSC message: %v", err)
continue
}
e.respChan <- msg
}
}
}
// parseOSCMessage parses raw bytes into an OSC message with improved error handling
func (e *engine) parseOSCMessage(data []byte) (*osc.Message, error) {
msg, err := e.parser.Parse(data)
if err != nil {
return nil, err
}
return msg, nil
}
// sendToAddress sends an OSC message to a specific address (enables replying to different ports)
func (e *engine) 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 = e.conn.WriteToUDP(data, addr)
return err
}

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

57
internal/xair/main.go Normal file
View File

@@ -0,0 +1,57 @@
package xair
import "fmt"
type Main struct {
client Client
}
func newMain(c Client) *Main {
return &Main{
client: c,
}
}
// Fader requests the current main L/R fader level
func (m *Main) Fader() (float64, error) {
err := m.client.SendMessage("/lr/mix/fader")
if err != nil {
return 0, err
}
resp := <-m.client.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
}
// SetFader sets the main L/R fader level
func (m *Main) SetFader(level float64) error {
return m.client.SendMessage("/lr/mix/fader", float32(mustDbInto(level)))
}
// Mute requests the current main L/R mute status
func (m *Main) Mute() (bool, error) {
err := m.client.SendMessage("/lr/mix/on")
if err != nil {
return false, err
}
resp := <-m.client.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return false, fmt.Errorf("unexpected argument type for main LR mute value")
}
return val == 0, nil
}
// SetMute sets the main L/R mute status
func (m *Main) SetMute(muted bool) error {
var value int32
if !muted {
value = 1
}
return m.client.SendMessage("/lr/mix/on", value)
}

View File

@@ -1,6 +1,3 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package xair
type InfoResponse struct {

View File

@@ -1,6 +1,3 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package xair
import (

152
internal/xair/strip.go Normal file
View File

@@ -0,0 +1,152 @@
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(index int) (bool, error) {
address := fmt.Sprintf(s.baseAddress, index) + "/mix/on"
err := s.client.SendMessage(address)
if err != nil {
return false, err
}
resp := <-s.client.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return false, fmt.Errorf("unexpected argument type for strip mute value")
}
return val == 0, nil
}
// SetMute sets the mute status of the specified strip (1-based indexing).
func (s *Strip) SetMute(strip int, muted bool) error {
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/on"
var value int32 = 0
if !muted {
value = 1
}
return s.client.SendMessage(address, value)
}
// Fader gets the fader level of the specified strip (1-based indexing).
func (s *Strip) Fader(strip int) (float64, error) {
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/fader"
err := s.client.SendMessage(address)
if err != nil {
return 0, err
}
resp := <-s.client.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for fader value")
}
return mustDbFrom(float64(val)), nil
}
// SetFader sets the fader level of the specified strip (1-based indexing).
func (s *Strip) SetFader(strip int, level float64) error {
address := fmt.Sprintf(s.baseAddress, strip) + "/mix/fader"
return s.client.SendMessage(address, float32(mustDbInto(level)))
}
// Name requests the name for a specific strip
func (s *Strip) Name(strip int) (string, error) {
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)
}
resp := <-s.client.respChan
val, ok := resp.Arguments[0].(string)
if !ok {
return "", fmt.Errorf("unexpected argument type for strip name value")
}
return val, nil
}
// SetName sets the name for a specific strip
func (s *Strip) SetName(strip int, name string) error {
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(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)
}
resp := <-s.client.respChan
val, ok := resp.Arguments[0].(int32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for strip color value")
}
return val, nil
}
// SetColor sets the color for a specific strip (0-15)
func (s *Strip) SetColor(strip int, color int32) error {
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(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)
}
resp := <-s.client.respChan
val, ok := resp.Arguments[0].(float32)
if !ok {
return 0, fmt.Errorf("unexpected argument type for strip send level value")
}
return mustDbFrom(float64(val)), nil
}
// SetSendLevel sets the sends level for a mixbus.
func (s *Strip) SetSendLevel(strip int, bus int, level float64) error {
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

@@ -1,10 +1,15 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
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: