Add output control and interactive demo with paired faders, encoder rings, meters, and LCDs

This commit is contained in:
Ian Gulliver
2026-02-10 21:38:04 -08:00
parent 14a361e7a0
commit fa7b310308
3 changed files with 166 additions and 5 deletions

View File

@@ -4,3 +4,4 @@ Never commit without permission.
Never mention Claude in commit messages. Single-line commit messages only. No Co-Authored-By line. Never mention Claude in commit messages. Single-line commit messages only. No Co-Authored-By line.
Never add comments to code. Never add comments to code.
Never ignore CLAUDE.md rules under any circumstances. Never ignore CLAUDE.md rules under any circumstances.
Never run tasks in the background.

78
main.go
View File

@@ -12,10 +12,42 @@ import (
"qrun/xtouch" "qrun/xtouch"
) )
var lcdColors = []xtouch.LCDColor{
xtouch.ColorRed,
xtouch.ColorGreen,
xtouch.ColorYellow,
xtouch.ColorBlue,
xtouch.ColorMagenta,
xtouch.ColorCyan,
xtouch.ColorWhite,
}
var (
faderValues [9]uint8
encoderValues [8]int
)
func updateLCD(out *xtouch.Output, ch uint8) {
color := lcdColors[encoderValues[ch]*len(lcdColors)/128]
out.SetLCD(ch, color, false, false,
fmt.Sprintf("E%-3d F%-3d", encoderValues[ch], faderValues[ch]),
fmt.Sprintf("Chan %d", ch+1))
}
func clamp(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}
func main() { func main() {
defer midi.CloseDriver() defer midi.CloseDriver()
port, err := xtouch.FindInPort("x-touch") inPort, err := xtouch.FindInPort("x-touch")
if err != nil { if err != nil {
fmt.Println("Available MIDI input ports:") fmt.Println("Available MIDI input ports:")
for _, p := range midi.GetInPorts() { for _, p := range midi.GetInPorts() {
@@ -25,13 +57,51 @@ func main() {
os.Exit(1) os.Exit(1)
} }
outPort, err := xtouch.FindOutPort("x-touch")
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
out, err := xtouch.NewOutput(outPort, xtouch.DeviceIDExtender)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
for i := uint8(0); i < 8; i++ {
updateLCD(out, i)
}
dec := &xtouch.Decoder{EncoderMode: xtouch.EncoderRelative} dec := &xtouch.Decoder{EncoderMode: xtouch.EncoderRelative}
fmt.Printf("Listening on: %s\n", port) fmt.Printf("Listening on: %s\n", inPort)
stop, err := midi.ListenTo(port, func(msg midi.Message, timestampms int32) { stop, err := midi.ListenTo(inPort, func(msg midi.Message, timestampms int32) {
if event := dec.Decode(msg); event != nil { event := dec.Decode(msg)
if event == nil {
return
}
fmt.Println(event) fmt.Println(event)
switch e := event.(type) {
case xtouch.FaderEvent:
if e.Fader > 7 {
return
}
faderValues[e.Fader] = e.Value
pair := e.Fader ^ 1
faderValues[pair] = e.Value
out.SetFader(pair, e.Value)
out.SetMeter(e.Fader, e.Value)
out.SetMeter(pair, e.Value)
updateLCD(out, e.Fader)
updateLCD(out, pair)
case xtouch.EncoderRelativeEvent:
encoderValues[e.Encoder] = clamp(encoderValues[e.Encoder]+e.Delta, 0, 127)
out.SetEncoderRing(e.Encoder, uint8(encoderValues[e.Encoder]))
updateLCD(out, e.Encoder)
} }
}) })
if err != nil { if err != nil {

90
xtouch/output.go Normal file
View File

@@ -0,0 +1,90 @@
package xtouch
import (
"fmt"
"gitlab.com/gomidi/midi/v2"
"gitlab.com/gomidi/midi/v2/drivers"
)
type LCDColor uint8
const (
ColorBlack LCDColor = 0
ColorRed LCDColor = 1
ColorGreen LCDColor = 2
ColorYellow LCDColor = 3
ColorBlue LCDColor = 4
ColorMagenta LCDColor = 5
ColorCyan LCDColor = 6
ColorWhite LCDColor = 7
)
type LEDState uint8
const (
LEDOff LEDState = 0
LEDFlash LEDState = 64
LEDOn LEDState = 127
)
type Output struct {
send func(msg midi.Message) error
DeviceID uint8
}
func NewOutput(port drivers.Out, deviceID uint8) (*Output, error) {
send, err := midi.SendTo(port)
if err != nil {
return nil, fmt.Errorf("open output port: %w", err)
}
return &Output{send: send, DeviceID: deviceID}, nil
}
func (o *Output) SetFader(fader uint8, value uint8) error {
cc := CCFaderFirst + fader
if fader == 8 {
cc = CCFaderMain
}
return o.send(midi.ControlChange(0, cc, value))
}
func (o *Output) SetButtonLED(button uint8, state LEDState) error {
return o.send(midi.NoteOn(0, button, uint8(state)))
}
func (o *Output) SetEncoderRing(encoder uint8, value uint8) error {
return o.send(midi.ControlChange(0, CCEncoderFirst+encoder, value))
}
func (o *Output) SetMeter(channel uint8, value uint8) error {
return o.send(midi.ControlChange(0, CCMeterFirst+channel, value))
}
func (o *Output) SetLCD(lcd uint8, color LCDColor, invertUpper bool, invertLower bool, upper string, lower string) error {
cc := uint8(color)
if invertUpper {
cc |= 0x10
}
if invertLower {
cc |= 0x20
}
upper = padOrTruncate(upper, 7)
lower = padOrTruncate(lower, 7)
data := []byte{0x00, 0x20, 0x32, o.DeviceID, 0x4C, lcd, cc}
data = append(data, []byte(upper)...)
data = append(data, []byte(lower)...)
return o.send(midi.SysEx(data))
}
func padOrTruncate(s string, n int) string {
if len(s) > n {
return s[:n]
}
for len(s) < n {
s += " "
}
return s
}