mirror of
https://github.com/onyx-and-iris/xair-cli.git
synced 2026-04-18 06:43:33 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 056ebc9116 | |||
| ce955deb38 | |||
| 6e3953e946 | |||
| 86ff40952b | |||
| 2590a43c18 | |||
| 0559899666 | |||
|
|
20fed03c48 | ||
| fc36e9bf5c | |||
| a9110f0986 | |||
| 72f43452a8 | |||
| 89ab8ee258 | |||
| c4a86adf14 | |||
| 1623b53cff | |||
| 98e131d4ad | |||
| c44413da6b | |||
| 90839d24a1 | |||
| 7d521e0111 | |||
| 625987759f | |||
| 08b232dcbf | |||
| 0b72556b7e | |||
| 9898c21197 | |||
| 64b4be032f | |||
| d894cc1317 | |||
| 4c4d52c74e | |||
| 7536c4fe24 | |||
| 2730f8dc5d | |||
| e7dd589243 | |||
| a0663350f8 | |||
| ad7c910180 | |||
| 615a95d9da | |||
| 19779ae4c1 | |||
| fc8c8ad69a | |||
| 205baf310f | |||
| d823aeeb8e | |||
| d1657e09ab | |||
| c851d0e804 |
52
README.md
52
README.md
@@ -1,4 +1,10 @@
|
||||
# Xair-CLI
|
||||
# xair-cli
|
||||
|
||||
### Installation
|
||||
|
||||
```console
|
||||
go install github.com/onyx-and-iris/xair-cli@latest
|
||||
```
|
||||
|
||||
### Use
|
||||
|
||||
@@ -14,6 +20,7 @@ Usage:
|
||||
Available Commands:
|
||||
bus Commands to control individual buses
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
headamp Commands to control headamp gain and phantom power
|
||||
help Help about any command
|
||||
main Commands to control the main output
|
||||
strip Commands to control individual strips
|
||||
@@ -24,6 +31,49 @@ Flags:
|
||||
-k, --kind string Kind of mixer (xair, x32) (default "xair")
|
||||
-l, --loglevel string Log level (debug, info, warn, error, fatal, panic) (default "warn")
|
||||
-p, --port int Port number of the X Air mixer (default 10024)
|
||||
-v, --version version for xair-cli
|
||||
|
||||
Use "xair-cli [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
*Fade out main LR all the way to -∞ over a 5s duration*
|
||||
|
||||
```console
|
||||
xair-cli main fadeout
|
||||
```
|
||||
|
||||
*enable phantom power and set the gain to 28.0dB over a 10s duration for strip 09*
|
||||
```console
|
||||
xair-cli headamp phantom 9 on
|
||||
|
||||
xair-cli headamp gain 9 28.0 --duration 10s
|
||||
```
|
||||
|
||||
*set strip 09 send level for bus 5 to -18.0dB*
|
||||
```console
|
||||
xair-cli strip send 9 5 -- -18.0
|
||||
```
|
||||
|
||||
*enable eq for strip 01*
|
||||
```console
|
||||
xair-cli strip eq on 1 true
|
||||
```
|
||||
|
||||
*rename bus 01 to 'vocal mix'*
|
||||
```console
|
||||
xair-cli bus name 1 'vocal mix'
|
||||
```
|
||||
|
||||
For every command/subcommand there exists a `--help` flag which you can use to get usage information.
|
||||
|
||||
|
||||
### Notes
|
||||
|
||||
This CLI is useful if just want to run some commands on the terminal using a single binary, no further downloads. However, there exists an alternative you should check out that has wider support of the XAir OSC protocol including support for operations like batch commands and network discovery (which I have no plans to implement on this CLI). Check out [dc-xair-cli](https://pypi.org/project/dc-xair-cli/) on pypi.
|
||||
|
||||
|
||||
### License
|
||||
|
||||
`xair-cli` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
||||
|
||||
924
cmd/bus.go
924
cmd/bus.go
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
211
cmd/headamp.go
Normal file
211
cmd/headamp.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/onyx-and-iris/xair-cli/internal/xair"
|
||||
"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.
|
||||
When setting gain, it will gradually increase from the current level to prevent
|
||||
sudden jumps that could cause feedback or equipment damage.
|
||||
|
||||
Examples:
|
||||
# Get gain level for headamp index 1
|
||||
xair-cli headamp gain 1
|
||||
# Set gain level for headamp index 1 to 3.5 dB (gradually over 5 seconds)
|
||||
xair-cli headamp gain 1 3.5
|
||||
# Set gain level for headamp index 1 to 3.5 dB over 10 seconds
|
||||
xair-cli headamp gain 1 3.5 --duration 10s`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client := ClientFromContext(cmd.Context())
|
||||
if client == nil {
|
||||
return fmt.Errorf("OSC client not found in context")
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("Please provide a headamp index")
|
||||
}
|
||||
|
||||
index := mustConvToInt(args[0])
|
||||
|
||||
if len(args) == 1 {
|
||||
gain, err := client.HeadAmp.Gain(index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting headamp gain level: %w", err)
|
||||
}
|
||||
cmd.Printf("Headamp %d Gain: %.2f dB\n", index, gain)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("Please provide a gain level in dB")
|
||||
}
|
||||
|
||||
targetLevel := mustConvToFloat64(args[1])
|
||||
|
||||
currentGain, err := client.HeadAmp.Gain(index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting current headamp gain level: %w", err)
|
||||
}
|
||||
|
||||
duration, err := cmd.Flags().GetDuration("duration")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting duration flag: %w", err)
|
||||
}
|
||||
|
||||
if currentGain == targetLevel {
|
||||
cmd.Printf("Headamp %d Gain already at %.2f dB\n", index, targetLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gradualGainAdjust(client, cmd, index, currentGain, targetLevel, duration); err != nil {
|
||||
return fmt.Errorf("Error adjusting headamp gain level: %w", err)
|
||||
}
|
||||
|
||||
cmd.Printf("Headamp %d Gain set to %.2f dB\n", index, targetLevel)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// gradualGainAdjust gradually adjusts gain from current to target over specified duration
|
||||
func gradualGainAdjust(
|
||||
client *xair.Client,
|
||||
cmd *cobra.Command,
|
||||
index int,
|
||||
currentGain, targetGain float64,
|
||||
duration time.Duration,
|
||||
) error {
|
||||
gainDiff := targetGain - currentGain
|
||||
|
||||
stepInterval := 100 * time.Millisecond
|
||||
totalSteps := int(duration / stepInterval)
|
||||
|
||||
if totalSteps < 1 {
|
||||
totalSteps = 1
|
||||
stepInterval = duration
|
||||
}
|
||||
|
||||
stepIncrement := gainDiff / float64(totalSteps)
|
||||
|
||||
log.Debugf("Adjusting Headamp %d gain from %.2f dB to %.2f dB over %v...\n",
|
||||
index, currentGain, targetGain, duration)
|
||||
|
||||
for step := 1; step <= totalSteps; step++ {
|
||||
newGain := currentGain + (stepIncrement * float64(step))
|
||||
|
||||
if step == totalSteps {
|
||||
newGain = targetGain
|
||||
}
|
||||
|
||||
err := client.HeadAmp.SetGain(index, newGain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if step%10 == 0 || step == totalSteps {
|
||||
log.Debugf(" Step %d/%d: %.2f dB\n", step, totalSteps, newGain)
|
||||
}
|
||||
|
||||
if step < totalSteps {
|
||||
time.Sleep(stepInterval)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client := ClientFromContext(cmd.Context())
|
||||
if client == nil {
|
||||
return fmt.Errorf("OSC client not found in context")
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("Please provide a headamp index")
|
||||
}
|
||||
|
||||
index := mustConvToInt(args[0])
|
||||
|
||||
if len(args) == 1 {
|
||||
enabled, err := client.HeadAmp.PhantomPower(index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting headamp phantom power status: %w", err)
|
||||
}
|
||||
status := "disabled"
|
||||
if enabled {
|
||||
status = "enabled"
|
||||
}
|
||||
cmd.Printf("Headamp %d Phantom Power is %s\n", index, status)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("Please provide phantom power status: on or off")
|
||||
}
|
||||
|
||||
var enable bool
|
||||
switch args[1] {
|
||||
case "on", "enable":
|
||||
enable = true
|
||||
case "off", "disable":
|
||||
enable = false
|
||||
default:
|
||||
return fmt.Errorf("Invalid phantom power status. Use 'on' or 'off'")
|
||||
}
|
||||
|
||||
err := client.HeadAmp.SetPhantomPower(index, enable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error setting headamp phantom power status: %w", err)
|
||||
}
|
||||
status := "disabled"
|
||||
if enable {
|
||||
status = "enabled"
|
||||
}
|
||||
|
||||
cmd.Printf("Headamp %d Phantom Power %s successfully\n", index, status)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(headampCmd)
|
||||
|
||||
headampCmd.AddCommand(headampGainCmd)
|
||||
headampGainCmd.Flags().DurationP("duration", "d", 5*time.Second, "Duration over which to gradually adjust gain")
|
||||
|
||||
headampCmd.AddCommand(headampPhantomPowerCmd)
|
||||
}
|
||||
120
cmd/main.go
120
cmd/main.go
@@ -1,126 +1,122 @@
|
||||
/*
|
||||
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
xair-cli main mute false`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client := ClientFromContext(cmd.Context())
|
||||
if client == nil {
|
||||
cmd.PrintErrln("OSC client not found in context")
|
||||
return
|
||||
return fmt.Errorf("OSC client not found in context")
|
||||
}
|
||||
|
||||
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
|
||||
return fmt.Errorf("Error getting main LR mute status: %w", err)
|
||||
}
|
||||
cmd.Printf("Main LR mute: %v\n", resp)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var muted bool
|
||||
if args[0] == "true" || args[0] == "1" {
|
||||
switch args[0] {
|
||||
case "true", "1":
|
||||
muted = true
|
||||
case "false", "0":
|
||||
muted = false
|
||||
default:
|
||||
return fmt.Errorf("Invalid mute status. Use true/false or 1/0")
|
||||
}
|
||||
|
||||
err := client.SetMainLRMute(muted)
|
||||
err := client.Main.SetMute(muted)
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error setting main LR mute status:", err)
|
||||
return
|
||||
return fmt.Errorf("Error setting main LR mute status: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println("Main LR mute status set successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// 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
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
xair-cli main fader -- -10.0`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client := ClientFromContext(cmd.Context())
|
||||
if client == nil {
|
||||
cmd.PrintErrln("OSC client not found in context")
|
||||
return
|
||||
return fmt.Errorf("OSC client not found in context")
|
||||
}
|
||||
|
||||
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
|
||||
return fmt.Errorf("Error getting main LR fader: %w", err)
|
||||
}
|
||||
cmd.Printf("Main LR fader: %.1f dB\n", resp)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
return fmt.Errorf("Error setting main LR fader: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println("Main LR fader set successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// 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 5s -- -90.0`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := ClientFromContext(cmd.Context())
|
||||
if client == nil {
|
||||
@@ -128,7 +124,7 @@ This command will fade out the main output to the specified dB level.
|
||||
return
|
||||
}
|
||||
|
||||
duration, err := cmd.Flags().GetFloat64("duration")
|
||||
duration, err := cmd.Flags().GetDuration("duration")
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error getting duration flag:", err)
|
||||
return
|
||||
@@ -140,7 +136,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
|
||||
@@ -154,33 +150,32 @@ This command will fade out the main output to the specified dB level.
|
||||
}
|
||||
|
||||
// Calculate delay per step to achieve exact duration
|
||||
stepDelay := time.Duration(duration*1000/totalSteps) * time.Millisecond
|
||||
stepDelay := time.Duration(duration.Seconds()*1000/totalSteps) * time.Millisecond
|
||||
|
||||
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
|
||||
}
|
||||
time.Sleep(stepDelay)
|
||||
}
|
||||
|
||||
cmd.Println("Main output faded out successfully")
|
||||
},
|
||||
}
|
||||
|
||||
// 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 5s -- 0.0`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := ClientFromContext(cmd.Context())
|
||||
if client == nil {
|
||||
@@ -188,7 +183,7 @@ This command will fade in the main output to the specified dB level.
|
||||
return
|
||||
}
|
||||
|
||||
duration, err := cmd.Flags().GetFloat64("duration")
|
||||
duration, err := cmd.Flags().GetDuration("duration")
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error getting duration flag:", err)
|
||||
return
|
||||
@@ -199,7 +194,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
|
||||
@@ -213,17 +208,18 @@ This command will fade in the main output to the specified dB level.
|
||||
}
|
||||
|
||||
// Calculate delay per step to achieve exact duration
|
||||
stepDelay := time.Duration(duration*1000/totalSteps) * time.Millisecond
|
||||
stepDelay := time.Duration(duration.Seconds()*1000/totalSteps) * time.Millisecond
|
||||
|
||||
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
|
||||
}
|
||||
time.Sleep(stepDelay)
|
||||
}
|
||||
|
||||
cmd.Println("Main output faded in successfully")
|
||||
},
|
||||
}
|
||||
@@ -235,7 +231,7 @@ func init() {
|
||||
|
||||
mainCmd.AddCommand(mainFaderCmd)
|
||||
mainCmd.AddCommand(mainFadeOutCmd)
|
||||
mainFadeOutCmd.Flags().Float64P("duration", "d", 5, "Duration for fade out in seconds")
|
||||
mainFadeOutCmd.Flags().DurationP("duration", "d", 5*time.Second, "Duration for fade out in seconds")
|
||||
mainCmd.AddCommand(mainFadeInCmd)
|
||||
mainFadeInCmd.Flags().Float64P("duration", "d", 5, "Duration for fade in in seconds")
|
||||
mainFadeInCmd.Flags().DurationP("duration", "d", 5*time.Second, "Duration for fade in in seconds")
|
||||
}
|
||||
|
||||
31
cmd/root.go
31
cmd/root.go
@@ -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
|
||||
}
|
||||
|
||||
1318
cmd/strip.go
1318
cmd/strip.go
File diff suppressed because it is too large
Load Diff
20
cmd/util.go
20
cmd/util.go
@@ -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 {
|
||||
@@ -22,3 +21,18 @@ func mustConvToInt(intStr string) int {
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// generic indexOf returns the index of elem in slice, or -1 if not found.
|
||||
func indexOf[T comparable](slice []T, elem T) int {
|
||||
for i, v := range slice {
|
||||
if v == elem {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// generic contains checks if elem is in slice.
|
||||
func contains[T comparable](slice []T, elem T) bool {
|
||||
return indexOf(slice, elem) != -1
|
||||
}
|
||||
|
||||
24
go.mod
24
go.mod
@@ -11,17 +11,17 @@ require (
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.4.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -29,15 +29,15 @@ require (
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
)
|
||||
|
||||
50
go.sum
50
go.sum
@@ -1,23 +1,23 @@
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.10.3 h1:3WoV9XN8uMEnFRZZ+vBPRy59TaIWa+gJodS4Vg5Fut0=
|
||||
github.com/charmbracelet/x/ansi v0.10.3/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU=
|
||||
github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -25,12 +25,12 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ=
|
||||
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -56,10 +56,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -79,13 +77,13 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
90
internal/xair/bus.go
Normal file
90
internal/xair/bus.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package xair
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Bus struct {
|
||||
baseAddress string
|
||||
client *Client
|
||||
Eq *Eq
|
||||
Comp *Comp
|
||||
}
|
||||
|
||||
func NewBus(c *Client) *Bus {
|
||||
return &Bus{
|
||||
baseAddress: c.addressMap["bus"],
|
||||
client: c,
|
||||
Eq: newEqForBus(c),
|
||||
Comp: newCompForBus(c),
|
||||
}
|
||||
}
|
||||
|
||||
// Mute requests the current mute status for a bus
|
||||
func (b *Bus) Mute(bus int) (bool, error) {
|
||||
address := fmt.Sprintf(b.baseAddress, 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
234
internal/xair/comp.go
Normal file
234
internal/xair/comp.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package xair
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Comp struct {
|
||||
client *Client
|
||||
baseAddress string
|
||||
}
|
||||
|
||||
// Factory function to create Comp instance for Strip
|
||||
func newCompForStrip(c *Client) *Comp {
|
||||
return &Comp{
|
||||
client: c,
|
||||
baseAddress: c.addressMap["strip"],
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function to create Comp instance for Bus
|
||||
func newCompForBus(c *Client) *Comp {
|
||||
return &Comp{
|
||||
client: c,
|
||||
baseAddress: c.addressMap["bus"],
|
||||
}
|
||||
}
|
||||
|
||||
// On retrieves the on/off status of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) On(index int) (bool, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/on"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unexpected argument type for Compressor on value")
|
||||
}
|
||||
return val != 0, nil
|
||||
}
|
||||
|
||||
// SetOn sets the on/off status of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetOn(index int, on bool) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/on"
|
||||
var value int32
|
||||
if on {
|
||||
value = 1
|
||||
}
|
||||
return c.client.SendMessage(address, value)
|
||||
}
|
||||
|
||||
// Mode retrieves the current mode of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) Mode(index int) (string, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/mode"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
possibleModes := []string{"comp", "exp"}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected argument type for Compressor mode value")
|
||||
}
|
||||
return possibleModes[val], nil
|
||||
}
|
||||
|
||||
// SetMode sets the mode of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetMode(index int, mode string) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/mode"
|
||||
possibleModes := []string{"comp", "exp"}
|
||||
return c.client.SendMessage(address, int32(indexOf(possibleModes, mode)))
|
||||
}
|
||||
|
||||
// Threshold retrieves the threshold value of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) Threshold(index int) (float64, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/thr"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Compressor threshold value")
|
||||
}
|
||||
return linGet(-60, 0, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetThreshold sets the threshold value of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetThreshold(index int, threshold float64) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/thr"
|
||||
return c.client.SendMessage(address, float32(linSet(-60, 0, threshold)))
|
||||
}
|
||||
|
||||
// Ratio retrieves the ratio value of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) Ratio(index int) (float32, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/ratio"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
possibleValues := []float32{1.1, 1.3, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.0, 10, 20, 100}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Compressor ratio value")
|
||||
}
|
||||
|
||||
return possibleValues[val], nil
|
||||
}
|
||||
|
||||
// SetRatio sets the ratio value of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetRatio(index int, ratio float64) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/ratio"
|
||||
possibleValues := []float32{1.1, 1.3, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.0, 10, 20, 100}
|
||||
|
||||
return c.client.SendMessage(address, int32(indexOf(possibleValues, float32(ratio))))
|
||||
}
|
||||
|
||||
// Attack retrieves the attack time of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) Attack(index int) (float64, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/attack"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Compressor attack value")
|
||||
}
|
||||
return linGet(0, 120, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetAttack sets the attack time of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetAttack(index int, attack float64) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/attack"
|
||||
return c.client.SendMessage(address, float32(linSet(0, 120, attack)))
|
||||
}
|
||||
|
||||
// Hold retrieves the hold time of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) Hold(index int) (float64, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/hold"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Compressor hold value")
|
||||
}
|
||||
return logGet(0.02, 2000, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetHold sets the hold time of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetHold(index int, hold float64) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/hold"
|
||||
return c.client.SendMessage(address, float32(logSet(0.02, 2000, hold)))
|
||||
}
|
||||
|
||||
// Release retrieves the release time of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) Release(index int) (float64, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/release"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Compressor release value")
|
||||
}
|
||||
return logGet(4, 4000, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetRelease sets the release time of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetRelease(index int, release float64) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/release"
|
||||
return c.client.SendMessage(address, float32(logSet(4, 4000, release)))
|
||||
}
|
||||
|
||||
// MakeUp retrieves the make-up gain of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) MakeUp(index int) (float64, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/mgain"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Compressor make-up gain value")
|
||||
}
|
||||
return linGet(0, 24, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetMakeUp sets the make-up gain of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetMakeUp(index int, makeUp float64) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/mgain"
|
||||
return c.client.SendMessage(address, float32(linSet(0, 24, makeUp)))
|
||||
}
|
||||
|
||||
// Mix retrieves the mix value of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) Mix(index int) (float64, error) {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/mix"
|
||||
err := c.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-c.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Compressor mix value")
|
||||
}
|
||||
return linGet(0, 100, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetMix sets the mix value of the Compressor for a specific strip or bus (1-based indexing).
|
||||
func (c *Comp) SetMix(index int, mix float64) error {
|
||||
address := fmt.Sprintf(c.baseAddress, index) + "/dyn/mix"
|
||||
return c.client.SendMessage(address, float32(linSet(0, 100, mix)))
|
||||
}
|
||||
97
internal/xair/engine.go
Normal file
97
internal/xair/engine.go
Normal 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
|
||||
}
|
||||
158
internal/xair/eq.go
Normal file
158
internal/xair/eq.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package xair
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Eq struct {
|
||||
client *Client
|
||||
baseAddress string
|
||||
}
|
||||
|
||||
// Factory function to create Eq instance for Strip
|
||||
func newEqForStrip(c *Client) *Eq {
|
||||
return &Eq{
|
||||
client: c,
|
||||
baseAddress: c.addressMap["strip"],
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function to create Eq instance for Bus
|
||||
func newEqForBus(c *Client) *Eq {
|
||||
return &Eq{
|
||||
client: c,
|
||||
baseAddress: c.addressMap["bus"],
|
||||
}
|
||||
}
|
||||
|
||||
// On retrieves the on/off status of the EQ for a specific strip or bus (1-based indexing).
|
||||
func (e *Eq) On(index int) (bool, error) {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + "/eq/on"
|
||||
err := e.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp := <-e.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unexpected argument type for EQ on value")
|
||||
}
|
||||
return val != 0, nil
|
||||
}
|
||||
|
||||
// SetOn sets the on/off status of the EQ for a specific strip or bus (1-based indexing).
|
||||
func (e *Eq) SetOn(index int, on bool) error {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + "/eq/on"
|
||||
var value int32
|
||||
if on {
|
||||
value = 1
|
||||
}
|
||||
return e.client.SendMessage(address, value)
|
||||
}
|
||||
|
||||
func (e *Eq) Mode(index int) (int, error) {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + "/eq/mode"
|
||||
err := e.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-e.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for EQ mode value")
|
||||
}
|
||||
return int(val), nil
|
||||
}
|
||||
|
||||
func (e *Eq) SetMode(index int, mode int) error {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + "/eq/mode"
|
||||
return e.client.SendMessage(address, int32(mode))
|
||||
}
|
||||
|
||||
// Gain retrieves the gain for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) Gain(index int, band int) (float64, error) {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/g", band)
|
||||
err := e.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-e.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for EQ gain value")
|
||||
}
|
||||
return linGet(-15, 15, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetGain sets the gain for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) SetGain(index int, band int, gain float64) error {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/g", band)
|
||||
return e.client.SendMessage(address, float32(linSet(-15, 15, gain)))
|
||||
}
|
||||
|
||||
// Frequency retrieves the frequency for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) Frequency(index int, band int) (float64, error) {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/f", band)
|
||||
err := e.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-e.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for EQ frequency value")
|
||||
}
|
||||
return logGet(20, 20000, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetFrequency sets the frequency for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) SetFrequency(index int, band int, frequency float64) error {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/f", band)
|
||||
return e.client.SendMessage(address, float32(logSet(20, 20000, frequency)))
|
||||
}
|
||||
|
||||
// Q retrieves the Q factor for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) Q(index int, band int) (float64, error) {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/q", band)
|
||||
err := e.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-e.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for EQ Q value")
|
||||
}
|
||||
return logGet(0.3, 10, 1.0-float64(val)), nil
|
||||
}
|
||||
|
||||
// SetQ sets the Q factor for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) SetQ(index int, band int, q float64) error {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/q", band)
|
||||
return e.client.SendMessage(address, float32(1.0-logSet(0.3, 10, q)))
|
||||
}
|
||||
|
||||
// Type retrieves the type for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) Type(index int, band int) (int, error) {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/type", band)
|
||||
err := e.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-e.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for EQ type value")
|
||||
}
|
||||
return int(val), nil
|
||||
}
|
||||
|
||||
// SetType sets the type for a specific EQ band on a strip or bus (1-based indexing).
|
||||
func (e *Eq) SetType(index int, band int, eqType int) error {
|
||||
address := fmt.Sprintf(e.baseAddress, index) + fmt.Sprintf("/eq/%d/type", band)
|
||||
return e.client.SendMessage(address, int32(eqType))
|
||||
}
|
||||
174
internal/xair/gate.go
Normal file
174
internal/xair/gate.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package xair
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Gate struct {
|
||||
client *Client
|
||||
baseAddress string
|
||||
}
|
||||
|
||||
func newGate(c *Client) *Gate {
|
||||
return &Gate{client: c, baseAddress: c.addressMap["strip"]}
|
||||
}
|
||||
|
||||
// On retrieves the on/off status of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) On(index int) (bool, error) {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/on"
|
||||
err := g.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp := <-g.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unexpected argument type for Gate on value")
|
||||
}
|
||||
return val != 0, nil
|
||||
}
|
||||
|
||||
// SetOn sets the on/off status of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) SetOn(index int, on bool) error {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/on"
|
||||
var value int32
|
||||
if on {
|
||||
value = 1
|
||||
}
|
||||
return g.client.SendMessage(address, value)
|
||||
}
|
||||
|
||||
// Mode retrieves the current mode of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) Mode(index int) (string, error) {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/mode"
|
||||
err := g.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
possibleModes := []string{"exp2", "exp3", "exp4", "gate", "duck"}
|
||||
|
||||
resp := <-g.client.respChan
|
||||
val, ok := resp.Arguments[0].(int32)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected argument type for Gate mode value")
|
||||
}
|
||||
return possibleModes[val], nil
|
||||
}
|
||||
|
||||
// SetMode sets the mode of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) SetMode(index int, mode string) error {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/mode"
|
||||
possibleModes := []string{"exp2", "exp3", "exp4", "gate", "duck"}
|
||||
|
||||
return g.client.SendMessage(address, int32(indexOf(possibleModes, mode)))
|
||||
}
|
||||
|
||||
// Threshold retrieves the threshold value of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) Threshold(index int) (float64, error) {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/thr"
|
||||
err := g.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-g.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Gate threshold value")
|
||||
}
|
||||
return linGet(-80, 0, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetThreshold sets the threshold value of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) SetThreshold(index int, threshold float64) error {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/thr"
|
||||
return g.client.SendMessage(address, float32(linSet(-80, 0, threshold)))
|
||||
}
|
||||
|
||||
// Range retrieves the range value of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) Range(index int) (float64, error) {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/range"
|
||||
err := g.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-g.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Gate range value")
|
||||
}
|
||||
return linGet(3, 60, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetRange sets the range value of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) SetRange(index int, rangeVal float64) error {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/range"
|
||||
return g.client.SendMessage(address, float32(linSet(3, 60, rangeVal)))
|
||||
}
|
||||
|
||||
// Attack retrieves the attack time of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) Attack(index int) (float64, error) {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/attack"
|
||||
err := g.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-g.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Gate attack value")
|
||||
}
|
||||
return linGet(0, 120, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetAttack sets the attack time of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) SetAttack(index int, attack float64) error {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/attack"
|
||||
return g.client.SendMessage(address, float32(linSet(0, 120, attack)))
|
||||
}
|
||||
|
||||
// Hold retrieves the hold time of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) Hold(index int) (float64, error) {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/hold"
|
||||
err := g.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-g.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Gate hold value")
|
||||
}
|
||||
return logGet(0.02, 2000, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetHold sets the hold time of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) SetHold(index int, hold float64) error {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/hold"
|
||||
return g.client.SendMessage(address, float32(logSet(0.02, 2000, hold)))
|
||||
}
|
||||
|
||||
// Release retrieves the release time of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) Release(index int) (float64, error) {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/release"
|
||||
err := g.client.SendMessage(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp := <-g.client.respChan
|
||||
val, ok := resp.Arguments[0].(float32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected argument type for Gate release value")
|
||||
}
|
||||
return logGet(5, 4000, float64(val)), nil
|
||||
}
|
||||
|
||||
// SetRelease sets the release time of the Gate for a specific strip (1-based indexing).
|
||||
func (g *Gate) SetRelease(index int, release float64) error {
|
||||
address := fmt.Sprintf(g.baseAddress, index) + "/gate/release"
|
||||
return g.client.SendMessage(address, float32(logSet(5, 4000, release)))
|
||||
}
|
||||
67
internal/xair/headamp.go
Normal file
67
internal/xair/headamp.go
Normal 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
57
internal/xair/main.go
Normal 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)
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
/*
|
||||
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
|
||||
*/
|
||||
package xair
|
||||
|
||||
type InfoResponse struct {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
/*
|
||||
LICENSE: https://github.com/onyx-and-iris/xair-cli/blob/main/LICENSE
|
||||
*/
|
||||
package xair
|
||||
|
||||
import (
|
||||
|
||||
136
internal/xair/strip.go
Normal file
136
internal/xair/strip.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package xair
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Strip struct {
|
||||
baseAddress string
|
||||
client *Client
|
||||
Gate *Gate
|
||||
Eq *Eq
|
||||
Comp *Comp
|
||||
}
|
||||
|
||||
func NewStrip(c *Client) *Strip {
|
||||
return &Strip{
|
||||
baseAddress: c.addressMap["strip"],
|
||||
client: c,
|
||||
Gate: newGate(c),
|
||||
Eq: newEqForStrip(c),
|
||||
Comp: newCompForStrip(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)))
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
/*
|
||||
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 logGet(min float64, max float64, value float64) float64 {
|
||||
return min * math.Exp(math.Log(max/min)*value)
|
||||
}
|
||||
|
||||
func logSet(min float64, max float64, value float64) float64 {
|
||||
return math.Log(value/min) / math.Log(max/min)
|
||||
}
|
||||
|
||||
func mustDbInto(db float64) float64 {
|
||||
switch {
|
||||
case db >= 10:
|
||||
@@ -43,3 +56,13 @@ func toFixed(num float64, precision int) float64 {
|
||||
output := math.Pow(10, float64(precision))
|
||||
return float64(math.Round(num*output)) / output
|
||||
}
|
||||
|
||||
// generic indexOf returns the index of elem in slice, or -1 if not found.
|
||||
func indexOf[T comparable](slice []T, elem T) int {
|
||||
for i, v := range slice {
|
||||
if v == elem {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user