Add xtouch library for reading X-Touch Extender input events

This commit is contained in:
Ian Gulliver
2026-02-10 21:24:15 -08:00
parent 130bab4bc8
commit 14a361e7a0
3 changed files with 267 additions and 21 deletions

6
CLAUDE.md Normal file
View File

@@ -0,0 +1,6 @@
Never use EnterPlanMode.
Never run `go build`. Use `go vet` or `go run` instead.
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.

50
main.go
View File

@@ -3,35 +3,45 @@ package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
"qrun/xtouch"
)
func main() {
defer midi.CloseDriver()
inPorts := midi.GetInPorts()
outPorts := midi.GetOutPorts()
fmt.Println("MIDI Input Ports:")
if len(inPorts) == 0 {
fmt.Println(" (none)")
port, err := xtouch.FindInPort("x-touch")
if err != nil {
fmt.Println("Available MIDI input ports:")
for _, p := range midi.GetInPorts() {
fmt.Printf(" %s\n", p)
}
for i, port := range inPorts {
fmt.Printf(" [%d] %s\n", i, port)
}
fmt.Println("\nMIDI Output Ports:")
if len(outPorts) == 0 {
fmt.Println(" (none)")
}
for i, port := range outPorts {
fmt.Printf(" [%d] %s\n", i, port)
}
if len(inPorts) == 0 && len(outPorts) == 0 {
fmt.Println("\nNo MIDI devices found.")
fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
os.Exit(1)
}
dec := &xtouch.Decoder{EncoderMode: xtouch.EncoderRelative}
fmt.Printf("Listening on: %s\n", port)
stop, err := midi.ListenTo(port, func(msg midi.Message, timestampms int32) {
if event := dec.Decode(msg); event != nil {
fmt.Println(event)
}
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error listening: %v\n", err)
os.Exit(1)
}
defer stop()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Println()
}

230
xtouch/xtouch.go Normal file
View File

@@ -0,0 +1,230 @@
package xtouch
import (
"fmt"
"strings"
"gitlab.com/gomidi/midi/v2"
"gitlab.com/gomidi/midi/v2/drivers"
)
const (
DeviceIDXTouch = 0x14
DeviceIDExtender = 0x15
)
const (
CCFootController = 4
CCFootSwitch1 = 64
CCFootSwitch2 = 67
CCFaderFirst = 70
CCFaderLast = 77
CCFaderMain = 78
CCEncoderFirst = 80
CCEncoderLast = 87
CCJogWheel = 88
CCMeterFirst = 90
CCMeterLast = 97
)
const (
NoteButtonFirst = 0
NoteButtonLast = 103
NoteFaderTouchFirst = 110
NoteFaderTouchLast = 117
NoteFaderTouchMain = 118
)
type Event interface {
String() string
}
type ButtonEvent struct {
Button uint8
Pressed bool
}
func (e ButtonEvent) String() string {
action := "released"
if e.Pressed {
action = "pressed"
}
return fmt.Sprintf("Button %d %s", e.Button, action)
}
type FaderEvent struct {
Fader uint8
Value uint8
}
func (e FaderEvent) String() string {
label := fmt.Sprintf("Fader %d", e.Fader)
if e.Fader == 8 {
label = "Fader main"
}
return fmt.Sprintf("%s = %d", label, e.Value)
}
type FaderTouchEvent struct {
Fader uint8
Touched bool
}
func (e FaderTouchEvent) String() string {
label := fmt.Sprintf("Fader %d", e.Fader)
if e.Fader == 8 {
label = "Fader main"
}
action := "released"
if e.Touched {
action = "touched"
}
return fmt.Sprintf("%s %s", label, action)
}
type EncoderAbsoluteEvent struct {
Encoder uint8
Value uint8
}
func (e EncoderAbsoluteEvent) String() string {
return fmt.Sprintf("Encoder %d = %d", e.Encoder, e.Value)
}
type EncoderRelativeEvent struct {
Encoder uint8
Delta int
}
func (e EncoderRelativeEvent) String() string {
return fmt.Sprintf("Encoder %d %+d", e.Encoder, e.Delta)
}
type JogWheelEvent struct {
Clockwise bool
}
func (e JogWheelEvent) String() string {
if e.Clockwise {
return "Jog wheel CW"
}
return "Jog wheel CCW"
}
type FootControllerEvent struct {
Value uint8
}
func (e FootControllerEvent) String() string {
return fmt.Sprintf("Foot controller = %d", e.Value)
}
type FootSwitchEvent struct {
Switch uint8
Pressed bool
}
func (e FootSwitchEvent) String() string {
action := "released"
if e.Pressed {
action = "pressed"
}
return fmt.Sprintf("Foot switch %d %s", e.Switch, action)
}
func FindInPort(substr string) (drivers.In, error) {
lower := strings.ToLower(substr)
for _, port := range midi.GetInPorts() {
if strings.Contains(strings.ToLower(port.String()), lower) {
return port, nil
}
}
return nil, fmt.Errorf("no MIDI input port matching %q", substr)
}
func FindOutPort(substr string) (drivers.Out, error) {
lower := strings.ToLower(substr)
for _, port := range midi.GetOutPorts() {
if strings.Contains(strings.ToLower(port.String()), lower) {
return port, nil
}
}
return nil, fmt.Errorf("no MIDI output port matching %q", substr)
}
type EncoderMode int
const (
EncoderAbsolute EncoderMode = iota
EncoderRelative
)
type Decoder struct {
EncoderMode EncoderMode
}
func (d *Decoder) Decode(msg midi.Message) Event {
switch {
case msg.Is(midi.NoteOnMsg):
var channel, key, velocity uint8
msg.GetNoteOn(&channel, &key, &velocity)
return decodeNoteOn(key, velocity)
case msg.Is(midi.NoteOffMsg):
var channel, key, velocity uint8
msg.GetNoteOff(&channel, &key, &velocity)
return decodeNoteOn(key, 0)
case msg.Is(midi.ControlChangeMsg):
var channel, controller, value uint8
msg.GetControlChange(&channel, &controller, &value)
return d.decodeCC(controller, value)
}
return nil
}
func decodeNoteOn(key, velocity uint8) Event {
pressed := velocity > 0
if key >= NoteButtonFirst && key <= NoteButtonLast {
return ButtonEvent{Button: key, Pressed: pressed}
}
if key >= NoteFaderTouchFirst && key <= NoteFaderTouchLast {
return FaderTouchEvent{Fader: key - NoteFaderTouchFirst, Touched: pressed}
}
if key == NoteFaderTouchMain {
return FaderTouchEvent{Fader: 8, Touched: pressed}
}
return nil
}
func (d *Decoder) decodeCC(controller, value uint8) Event {
switch {
case controller >= CCFaderFirst && controller <= CCFaderLast:
return FaderEvent{Fader: controller - CCFaderFirst, Value: value}
case controller == CCFaderMain:
return FaderEvent{Fader: 8, Value: value}
case controller >= CCEncoderFirst && controller <= CCEncoderLast:
enc := controller - CCEncoderFirst
if d.EncoderMode == EncoderRelative {
delta := 0
switch value {
case 65:
delta = 1
case 1:
delta = -1
}
return EncoderRelativeEvent{Encoder: enc, Delta: delta}
}
return EncoderAbsoluteEvent{Encoder: enc, Value: value}
case controller == CCJogWheel:
return JogWheelEvent{Clockwise: value == 65}
case controller == CCFootController:
return FootControllerEvent{Value: value}
case controller == CCFootSwitch1:
return FootSwitchEvent{Switch: 1, Pressed: value > 0}
case controller == CCFootSwitch2:
return FootSwitchEvent{Switch: 2, Pressed: value > 0}
}
return nil
}