From fa7b31030871fdb1c4fafc588df786a059bac9d1 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 10 Feb 2026 21:38:04 -0800 Subject: [PATCH] Add output control and interactive demo with paired faders, encoder rings, meters, and LCDs --- CLAUDE.md | 1 + main.go | 80 +++++++++++++++++++++++++++++++++++++++--- xtouch/output.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 xtouch/output.go diff --git a/CLAUDE.md b/CLAUDE.md index f2f721e..0290bb4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 add comments to code. Never ignore CLAUDE.md rules under any circumstances. +Never run tasks in the background. diff --git a/main.go b/main.go index 8790433..642d2bb 100644 --- a/main.go +++ b/main.go @@ -12,10 +12,42 @@ import ( "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() { defer midi.CloseDriver() - port, err := xtouch.FindInPort("x-touch") + inPort, err := xtouch.FindInPort("x-touch") if err != nil { fmt.Println("Available MIDI input ports:") for _, p := range midi.GetInPorts() { @@ -25,13 +57,51 @@ func main() { 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} - 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) { - if event := dec.Decode(msg); event != nil { - fmt.Println(event) + stop, err := midi.ListenTo(inPort, func(msg midi.Message, timestampms int32) { + event := dec.Decode(msg) + if event == nil { + return + } + 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 { diff --git a/xtouch/output.go b/xtouch/output.go new file mode 100644 index 0000000..b8d64d0 --- /dev/null +++ b/xtouch/output.go @@ -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 +}