Add xtouch library for reading X-Touch Extender input events
This commit is contained in:
6
CLAUDE.md
Normal file
6
CLAUDE.md
Normal 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.
|
||||||
52
main.go
52
main.go
@@ -3,35 +3,45 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"gitlab.com/gomidi/midi/v2"
|
"gitlab.com/gomidi/midi/v2"
|
||||||
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
||||||
|
|
||||||
|
"qrun/xtouch"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer midi.CloseDriver()
|
defer midi.CloseDriver()
|
||||||
|
|
||||||
inPorts := midi.GetInPorts()
|
port, err := xtouch.FindInPort("x-touch")
|
||||||
outPorts := midi.GetOutPorts()
|
if err != nil {
|
||||||
|
fmt.Println("Available MIDI input ports:")
|
||||||
fmt.Println("MIDI Input Ports:")
|
for _, p := range midi.GetInPorts() {
|
||||||
if len(inPorts) == 0 {
|
fmt.Printf(" %s\n", p)
|
||||||
fmt.Println(" (none)")
|
}
|
||||||
}
|
fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
|
||||||
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.")
|
|
||||||
os.Exit(1)
|
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
230
xtouch/xtouch.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user