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 package cmd
import ( import (
"fmt"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// busCmd represents the bus command // busCmd represents the bus command.
var busCmd = &cobra.Command{ var busCmd = &cobra.Command{
Use: "bus",
Short: "Commands to control individual buses", Short: "Commands to control individual buses",
Long: `Commands to control individual buses of the XAir mixer, including mute status.`, Long: `Commands to control individual buses of the XAir mixer, including mute status.`,
Run: func(cmd *cobra.Command, args []string) { Use: "bus",
fmt.Println("bus called") Run: func(cmd *cobra.Command, _ []string) {
cmd.Help()
}, },
} }
// busMuteCmd represents the bus mute command // busMuteCmd represents the bus mute command.
var busMuteCmd = &cobra.Command{ var busMuteCmd = &cobra.Command{
Use: "mute",
Short: "Get or set the bus mute status", Short: "Get or set the bus mute status",
Long: `Get or set the mute status of a specific bus.`, Long: `Get or set the mute status of a specific bus.`,
Use: "mute [bus number] [true|false]",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -49,7 +45,7 @@ var busMuteCmd = &cobra.Command{
return return
} }
err := client.SetBusMute(busNum, muted) err := client.Bus.SetMute(busNum, muted)
if err != nil { if err != nil {
cmd.PrintErrln("Error setting bus mute status:", err) cmd.PrintErrln("Error setting bus mute status:", err)
return 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{ var busFaderCmd = &cobra.Command{
Use: "fader",
Short: "Get or set the bus fader level", 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) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -70,14 +73,15 @@ var busFaderCmd = &cobra.Command{
return return
} }
busIndex := mustConvToInt(args[0])
if len(args) == 1 { if len(args) == 1 {
busNum := mustConvToInt(args[0]) level, err := client.Bus.Fader(busIndex)
level, err := client.BusFader(busNum)
if err != nil { if err != nil {
cmd.PrintErrln("Error getting bus fader level:", err) cmd.PrintErrln("Error getting bus fader level:", err)
return return
} }
cmd.Printf("Bus %d fader level: %.1f dB\n", busNum, level) cmd.Printf("Bus %d fader level: %.1f dB\n", busIndex, level)
return return
} }
@@ -86,28 +90,24 @@ var busFaderCmd = &cobra.Command{
return return
} }
busNum := mustConvToInt(args[0])
level := mustConvToFloat64(args[1]) level := mustConvToFloat64(args[1])
err := client.SetBusFader(busNum, level) err := client.Bus.SetFader(busIndex, level)
if err != nil { if err != nil {
cmd.PrintErrln("Error setting bus fader level:", err) cmd.PrintErrln("Error setting bus fader level:", err)
return 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{ var busFadeOutCmd = &cobra.Command{
Use: "fadeout",
Short: "Fade out the bus fader over a specified duration", 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. 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]",
For example: Example: ` # Fade out bus 1 over 5 seconds
# Fade out bus 1 over 5 seconds xair-cli bus fadeout 1 --duration 5 -- -90.0`,
xair-cli bus fadeout 1 --duration 5
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -133,7 +133,7 @@ For example:
target = mustConvToFloat64(args[1]) target = mustConvToFloat64(args[1])
} }
currentFader, err := client.BusFader(busIndex) currentFader, err := client.Bus.Fader(busIndex)
if err != nil { if err != nil {
cmd.PrintErrln("Error getting current bus fader level:", err) cmd.PrintErrln("Error getting current bus fader level:", err)
return return
@@ -142,7 +142,7 @@ For example:
// Calculate total steps needed to reach target dB // Calculate total steps needed to reach target dB
totalSteps := float64(currentFader - target) totalSteps := float64(currentFader - target)
if totalSteps <= 0 { if totalSteps <= 0 {
cmd.Println("Bus is already faded out") cmd.Println("Bus is already at or below target level")
return return
} }
@@ -150,7 +150,7 @@ For example:
for currentFader > target { for currentFader > target {
currentFader -= 1.0 currentFader -= 1.0
err := client.SetBusFader(busIndex, currentFader) err := client.Bus.SetFader(busIndex, currentFader)
if err != nil { if err != nil {
cmd.PrintErrln("Error setting bus fader level:", err) cmd.PrintErrln("Error setting bus fader level:", err)
return 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{ var busFadeInCmd = &cobra.Command{
Use: "fadein",
Short: "Fade in the bus fader over a specified duration", 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. 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]",
For example: Example: ` # Fade in bus 1 over 5 seconds
# Fade in bus 1 over 5 seconds xair-cli bus fadein 1 --duration 5 -- 0.0`,
xair-cli bus fadein 1 --duration 5
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -197,7 +194,7 @@ For example:
target = mustConvToFloat64(args[1]) target = mustConvToFloat64(args[1])
} }
currentFader, err := client.BusFader(busIndex) currentFader, err := client.Bus.Fader(busIndex)
if err != nil { if err != nil {
cmd.PrintErrln("Error getting current bus fader level:", err) cmd.PrintErrln("Error getting current bus fader level:", err)
return return
@@ -214,7 +211,7 @@ For example:
for currentFader < target { for currentFader < target {
currentFader += 1.0 currentFader += 1.0
err := client.SetBusFader(busIndex, currentFader) err := client.Bus.SetFader(busIndex, currentFader)
if err != nil { if err != nil {
cmd.PrintErrln("Error setting bus fader level:", err) cmd.PrintErrln("Error setting bus fader level:", err)
return 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() { func init() {
rootCmd.AddCommand(busCmd) rootCmd.AddCommand(busCmd)
@@ -233,7 +274,9 @@ func init() {
busCmd.AddCommand(busFaderCmd) busCmd.AddCommand(busFaderCmd)
busCmd.AddCommand(busFadeOutCmd) 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) 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 package cmd
import ( import (
@@ -11,10 +8,12 @@ import (
type clientKey string type clientKey string
// WithContext returns a new context with the provided xair.Client.
func WithContext(ctx context.Context, client *xair.Client) context.Context { func WithContext(ctx context.Context, client *xair.Client) context.Context {
return context.WithValue(ctx, clientKey("oscClient"), client) return context.WithValue(ctx, clientKey("oscClient"), client)
} }
// ClientFromContext retrieves the xair.Client from the context.
func ClientFromContext(ctx context.Context) *xair.Client { func ClientFromContext(ctx context.Context) *xair.Client {
if client, ok := ctx.Value(clientKey("oscClient")).(*xair.Client); ok { if client, ok := ctx.Value(clientKey("oscClient")).(*xair.Client); ok {
return client 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 package cmd
import ( import (
@@ -9,35 +6,33 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// mainCmd represents the main command // mainCmd represents the main command.
var mainCmd = &cobra.Command{ var mainCmd = &cobra.Command{
Use: "main",
Short: "Commands to control the main output", Short: "Commands to control the main output",
Long: `Commands to control the main output of the XAir mixer, including fader level and mute status.`, 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() cmd.Help()
}, },
} }
// mainMuteCmd represents the main mute command.
var mainMuteCmd = &cobra.Command{ var mainMuteCmd = &cobra.Command{
Use: "mute",
Short: "Get or set the main LR mute status", Short: "Get or set the main LR mute status",
Long: `Get or set the main L/R mute status. Long: `Get or set the main L/R mute status.
If no argument is provided, the current mute status is retrieved. 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 "true" or "1" is provided as an argument, the main output is muted.
If "false" or "0" is provided, the main output is unmuted. If "false" or "0" is provided, the main output is unmuted.`,
Use: "mute [true|false]",
For example: Example: ` # Get the current main LR mute status
# Get the current main LR mute status
xair-cli main mute xair-cli main mute
# Mute the main output # Mute the main output
xair-cli main mute true xair-cli main mute true
# Unmute the main output # Unmute the main output
xair-cli main mute false xair-cli main mute false`,
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -46,7 +41,7 @@ For example:
} }
if len(args) == 0 { if len(args) == 0 {
resp, err := client.MainLRMute() resp, err := client.Main.Mute()
if err != nil { if err != nil {
cmd.PrintErrln("Error getting main LR mute status:", err) cmd.PrintErrln("Error getting main LR mute status:", err)
return return
@@ -56,11 +51,17 @@ For example:
} }
var muted bool var muted bool
if args[0] == "true" || args[0] == "1" { switch args[0] {
case "true", "1":
muted = true 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 { if err != nil {
cmd.PrintErrln("Error setting main LR mute status:", err) cmd.PrintErrln("Error setting main LR mute status:", err)
return return
@@ -69,21 +70,19 @@ For example:
}, },
} }
// mainFaderCmd represents the main fader command.
var mainFaderCmd = &cobra.Command{ var mainFaderCmd = &cobra.Command{
Use: "fader",
Short: "Set or get the main LR fader level", Short: "Set or get the main LR fader level",
Long: `Set or get the main L/R fader level in dB. Long: `Set or get the main L/R fader level in dB.
If no argument is provided, the current fader level is retrieved. 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. If a dB value is provided as an argument, the fader level is set to that value.`,
Use: "fader [level in dB]",
For example: Example: ` # Get the current main LR fader level
# Get the current main LR fader level
xair-cli main fader xair-cli main fader
# Set the main LR fader level to -10.0 dB # 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) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -92,7 +91,7 @@ For example:
} }
if len(args) == 0 { if len(args) == 0 {
resp, err := client.MainLRFader() resp, err := client.Main.Fader()
if err != nil { if err != nil {
cmd.PrintErrln("Error getting main LR fader:", err) cmd.PrintErrln("Error getting main LR fader:", err)
return return
@@ -101,7 +100,7 @@ For example:
return return
} }
err := client.SetMainLRFader(mustConvToFloat64(args[0])) err := client.Main.SetFader(mustConvToFloat64(args[0]))
if err != nil { if err != nil {
cmd.PrintErrln("Error setting main LR fader:", err) cmd.PrintErrln("Error setting main LR fader:", err)
return return
@@ -110,17 +109,16 @@ For example:
}, },
} }
// mainFadeOutCmd represents the main fadeout command.
var mainFadeOutCmd = &cobra.Command{ var mainFadeOutCmd = &cobra.Command{
Use: "fadeout [target_db]",
Short: "Fade out the main output", Short: "Fade out the main output",
Long: `Fade out the main output over a specified duration. 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. 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) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -140,7 +138,7 @@ This command will fade out the main output to the specified dB level.
target = mustConvToFloat64(args[0]) target = mustConvToFloat64(args[0])
} }
currentFader, err := client.MainLRFader() currentFader, err := client.Main.Fader()
if err != nil { if err != nil {
cmd.PrintErrln("Error getting current main LR fader:", err) cmd.PrintErrln("Error getting current main LR fader:", err)
return return
@@ -158,7 +156,7 @@ This command will fade out the main output to the specified dB level.
for currentFader > target { for currentFader > target {
currentFader -= 1.0 currentFader -= 1.0
err = client.SetMainLRFader(currentFader) err = client.Main.SetFader(currentFader)
if err != nil { if err != nil {
cmd.PrintErrln("Error setting main LR fader:", err) cmd.PrintErrln("Error setting main LR fader:", err)
return 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{ var mainFadeInCmd = &cobra.Command{
Use: "fadein [target_db]",
Short: "Fade in the main output", Short: "Fade in the main output",
Long: `Fade in the main output over a specified duration. 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. 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) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -199,7 +195,7 @@ This command will fade in the main output to the specified dB level.
target = mustConvToFloat64(args[0]) target = mustConvToFloat64(args[0])
} }
currentFader, err := client.MainLRFader() currentFader, err := client.Main.Fader()
if err != nil { if err != nil {
cmd.PrintErrln("Error getting current main LR fader:", err) cmd.PrintErrln("Error getting current main LR fader:", err)
return return
@@ -217,7 +213,7 @@ This command will fade in the main output to the specified dB level.
for currentFader < target { for currentFader < target {
currentFader += 1.0 currentFader += 1.0
err = client.SetMainLRFader(currentFader) err = client.Main.SetFader(currentFader)
if err != nil { if err != nil {
cmd.PrintErrln("Error setting main LR fader:", err) cmd.PrintErrln("Error setting main LR fader:", err)
return return

View File

@@ -1,10 +1,8 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package cmd package cmd
import ( import (
"os" "os"
"runtime/debug"
"strings" "strings"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
@@ -14,14 +12,17 @@ import (
"github.com/onyx-and-iris/xair-cli/internal/xair" "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{ var rootCmd = &cobra.Command{
Use: "xair-cli", Use: "xair-cli",
Short: "A command-line utility to interact with Behringer X Air mixers via OSC", 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 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 to Behringer X Air mixers for remote control and configuration. It supports
various commands to manage mixer settings directly from the terminal.`, 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")) level, err := log.ParseLevel(viper.GetString("loglevel"))
if err != nil { if err != nil {
return err return err
@@ -29,7 +30,7 @@ various commands to manage mixer settings directly from the terminal.`,
log.SetLevel(level) log.SetLevel(level)
kind := viper.GetString("kind") 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") { if kind == "x32" && !viper.IsSet("port") {
viper.Set("port", 10023) viper.Set("port", 10023)
@@ -55,18 +56,20 @@ various commands to manage mixer settings directly from the terminal.`,
return nil return nil
}, },
PersistentPostRunE: func(cmd *cobra.Command, args []string) error { PersistentPostRunE: func(cmd *cobra.Command, _ []string) error {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client != nil { if client != nil {
client.Stop() client.Stop()
} }
return nil return nil
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, _ []string) {
cmd.Help() 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() { func Execute() {
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {
@@ -89,3 +92,15 @@ func init() {
viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel")) viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
viper.BindPFlag("kind", rootCmd.PersistentFlags().Lookup("kind")) 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 package cmd
import ( import (
"fmt" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// stripCmd represents the strip command // stripCmd represents the strip command.
var stripCmd = &cobra.Command{ var stripCmd = &cobra.Command{
Use: "strip",
Short: "Commands to control individual strips", Short: "Commands to control individual strips",
Long: `Commands to control individual strips of the XAir mixer, including fader level and mute status.`, Long: `Commands to control individual strips of the XAir mixer, including fader level and mute status.`,
Run: func(cmd *cobra.Command, args []string) { Use: "strip",
fmt.Println("strip called") Run: func(cmd *cobra.Command, _ []string) {
cmd.Help()
}, },
} }
// stripMuteCmd represents the strip mute command.
var stripMuteCmd = &cobra.Command{ var stripMuteCmd = &cobra.Command{
Use: "mute",
Short: "Get or set the mute status of a strip", Short: "Get or set the mute status of a strip",
Long: `Get or set the mute status of a specific strip. Long: `Get or set the mute status of a specific strip.
If no argument is provided, the current mute status is retrieved. 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 "true" or "1" is provided as an argument, the strip is muted.
If "false" or "0" is provided, the strip is unmuted. If "false" or "0" is provided, the strip is unmuted.`,
Use: "mute [strip number] [true|false]",
For example: Example: ` # Get the current mute status of strip 1
# Get the current mute status of strip 1
xair-cli strip mute 1 xair-cli strip mute 1
# Mute strip 1 # Mute strip 1
xair-cli strip mute 1 true xair-cli strip mute 1 true
# Unmute strip 1 # Unmute strip 1
xair-cli strip mute 1 false xair-cli strip mute 1 false`,
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
client := ClientFromContext(cmd.Context()) client := ClientFromContext(cmd.Context())
if client == nil { if client == nil {
@@ -52,7 +47,7 @@ For example:
stripIndex := mustConvToInt(args[0]) stripIndex := mustConvToInt(args[0])
if len(args) == 1 { if len(args) == 1 {
resp, err := client.StripMute(stripIndex) resp, err := client.Strip.Mute(stripIndex)
if err != nil { if err != nil {
cmd.PrintErrln("Error getting strip mute status:", err) cmd.PrintErrln("Error getting strip mute status:", err)
return return
@@ -72,7 +67,7 @@ For example:
return return
} }
err := client.SetStripMute(stripIndex, muted) err := client.Strip.SetMute(stripIndex, muted)
if err != nil { if err != nil {
cmd.PrintErrln("Error setting strip mute status:", err) cmd.PrintErrln("Error setting strip mute status:", err)
return 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() { func init() {
rootCmd.AddCommand(stripCmd) rootCmd.AddCommand(stripCmd)
stripCmd.AddCommand(stripMuteCmd) 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 package cmd
import ( import (
"strconv" "strconv"
) )
// mustConvToFloat64 converts a string to float64, panicking on error.
func mustConvToFloat64(floatStr string) float64 { func mustConvToFloat64(floatStr string) float64 {
level, err := strconv.ParseFloat(floatStr, 64) level, err := strconv.ParseFloat(floatStr, 64)
if err != nil { if err != nil {
@@ -15,6 +13,7 @@ func mustConvToFloat64(floatStr string) float64 {
return level return level
} }
// mustConvToInt converts a string to int, panicking on error.
func mustConvToInt(intStr string) int { func mustConvToInt(intStr string) int {
val, err := strconv.Atoi(intStr) val, err := strconv.Atoi(intStr)
if err != nil { if err != nil {

View File

@@ -1,11 +1,15 @@
package xair package xair
var xairAddressMap = map[string]string{ var xairAddressMap = map[string]string{
"bus": "/bus/%01d", "strip": "/ch/%02d",
"bus": "/bus/%01d",
"headamp": "/headamp/%02d",
} }
var x32AddressMap = map[string]string{ var x32AddressMap = map[string]string{
"bus": "/bus/%02d", "strip": "/ch/%02d",
"bus": "/bus/%02d",
"headamp": "/headamp/%02d",
} }
func addressMapForMixerKind(kind MixerKind) map[string]string { 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 package xair
import ( import (
"fmt" "fmt"
"net" "net"
"time"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
@@ -17,20 +13,12 @@ type parser interface {
Parse(data []byte) (*osc.Message, error) 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 { type Client struct {
engine engine
Main *Main
Strip *Strip
Bus *Bus
HeadAmp *HeadAmp
} }
// NewClient creates a new XAirClient instance // NewClient creates a new XAirClient instance
@@ -67,104 +55,34 @@ func NewClient(mixerIP string, mixerPort int, opts ...Option) (*Client, error) {
opt(e) opt(e)
} }
return &Client{ c := &Client{
engine: *e, 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 // Start begins listening for messages in a goroutine
func (c *Client) StartListening() { func (c *Client) StartListening() {
go c.receiveLoop() go c.engine.receiveLoop()
log.Debugf("Started listening on %s...", c.conn.LocalAddr().String()) log.Debugf("Started listening on %s...", c.engine.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
} }
// Stop stops the client and closes the connection // Stop stops the client and closes the connection
func (c *Client) Stop() { func (c *Client) Stop() {
close(c.done) close(c.engine.done)
if c.conn != nil { if c.engine.conn != nil {
c.conn.Close() c.engine.conn.Close()
} }
} }
// SendMessage sends an OSC message to the mixer using the unified connection // SendMessage sends an OSC message to the mixer using the unified connection
func (c *Client) SendMessage(address string, args ...any) error { func (c *Client) SendMessage(address string, args ...any) error {
return c.SendToAddress(c.mixerAddr, address, args...) return c.engine.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
} }
// RequestInfo requests mixer information // RequestInfo requests mixer information
@@ -193,221 +111,3 @@ func (c *Client) KeepAlive() error {
func (c *Client) RequestStatus() error { func (c *Client) RequestStatus() error {
return c.SendMessage("/status") 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 package xair
type InfoResponse struct { type InfoResponse struct {

View File

@@ -1,6 +1,3 @@
/*
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
*/
package xair package xair
import ( 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 package xair
import "math" 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 { func mustDbInto(db float64) float64 {
switch { switch {
case db >= 10: case db >= 10: