Move libraries to lib/ and PDF to docs/
This commit is contained in:
204
lib/streamdeck/streamdeck.go
Normal file
204
lib/streamdeck/streamdeck.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package streamdeck
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"time"
|
||||
|
||||
xdraw "golang.org/x/image/draw"
|
||||
|
||||
"rafaelmartins.com/p/usbhid"
|
||||
)
|
||||
|
||||
const (
|
||||
elgatoVendorID = 0x0fd9
|
||||
keyCount = 32
|
||||
keyRows = 4
|
||||
keyCols = 8
|
||||
keySize = 96
|
||||
keyStart = 3
|
||||
)
|
||||
|
||||
var xlProductIDs = map[uint16]bool{
|
||||
0x006c: true,
|
||||
0x008f: true,
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
dev *usbhid.Device
|
||||
}
|
||||
|
||||
func Open() (*Device, error) {
|
||||
devices, err := usbhid.Enumerate(func(dev *usbhid.Device) bool {
|
||||
return dev.VendorId() == elgatoVendorID && xlProductIDs[dev.ProductId()]
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("streamdeck: enumerate: %w", err)
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
return nil, fmt.Errorf("streamdeck: no XL device found")
|
||||
}
|
||||
|
||||
dev := devices[0]
|
||||
if err := dev.Open(true); err != nil {
|
||||
return nil, fmt.Errorf("streamdeck: open: %w", err)
|
||||
}
|
||||
|
||||
return &Device{dev: dev}, nil
|
||||
}
|
||||
|
||||
func (d *Device) Close() error {
|
||||
return d.dev.Close()
|
||||
}
|
||||
|
||||
func (d *Device) SerialNumber() string {
|
||||
return d.dev.SerialNumber()
|
||||
}
|
||||
|
||||
func (d *Device) Product() string {
|
||||
return d.dev.Product()
|
||||
}
|
||||
|
||||
func (d *Device) FirmwareVersion() (string, error) {
|
||||
buf, err := d.dev.GetFeatureReport(5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, _, _ := bytes.Cut(buf[5:], []byte{0})
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (d *Device) SetBrightness(perc byte) error {
|
||||
if perc > 100 {
|
||||
perc = 100
|
||||
}
|
||||
pl := make([]byte, d.dev.GetFeatureReportLength())
|
||||
pl[0] = 0x08
|
||||
pl[1] = perc
|
||||
return d.dev.SetFeatureReport(3, pl)
|
||||
}
|
||||
|
||||
func (d *Device) Reset() error {
|
||||
pl := make([]byte, d.dev.GetFeatureReportLength())
|
||||
pl[0] = 0x02
|
||||
return d.dev.SetFeatureReport(3, pl)
|
||||
}
|
||||
|
||||
func (d *Device) SetKeyColor(key int, c color.Color) error {
|
||||
img := image.NewRGBA(image.Rect(0, 0, keySize, keySize))
|
||||
xdraw.Draw(img, img.Bounds(), &image.Uniform{c}, image.Point{}, xdraw.Src)
|
||||
return d.SetKeyImage(key, img)
|
||||
}
|
||||
|
||||
func (d *Device) SetKeyImage(key int, img image.Image) error {
|
||||
if key < 0 || key >= keyCount {
|
||||
return fmt.Errorf("streamdeck: invalid key %d", key)
|
||||
}
|
||||
|
||||
scaled := image.NewRGBA(image.Rect(0, 0, keySize, keySize))
|
||||
xdraw.BiLinear.Scale(scaled, scaled.Bounds(), img, img.Bounds(), xdraw.Over, nil)
|
||||
|
||||
flipped := image.NewRGBA(scaled.Bounds())
|
||||
for y := 0; y < keySize; y++ {
|
||||
for x := 0; x < keySize; x++ {
|
||||
flipped.Set(keySize-1-x, keySize-1-y, scaled.At(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, flipped, &jpeg.Options{Quality: 100}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.sendImage(byte(key), buf.Bytes())
|
||||
}
|
||||
|
||||
func (d *Device) ClearKey(key int) error {
|
||||
return d.SetKeyColor(key, color.Black)
|
||||
}
|
||||
|
||||
func (d *Device) ClearAllKeys() error {
|
||||
for i := 0; i < keyCount; i++ {
|
||||
if err := d.ClearKey(i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) sendImage(key byte, imgData []byte) error {
|
||||
reportLen := d.dev.GetOutputReportLength()
|
||||
hdrLen := uint16(7)
|
||||
payloadLen := reportLen - hdrLen
|
||||
|
||||
var page byte
|
||||
for start := uint16(0); start < uint16(len(imgData)); page++ {
|
||||
end := start + payloadLen
|
||||
last := byte(0)
|
||||
if end >= uint16(len(imgData)) {
|
||||
end = uint16(len(imgData))
|
||||
last = 1
|
||||
}
|
||||
|
||||
chunk := imgData[start:end]
|
||||
hdr := []byte{
|
||||
0x07,
|
||||
key,
|
||||
last,
|
||||
byte(len(chunk)),
|
||||
byte(len(chunk) >> 8),
|
||||
page,
|
||||
0,
|
||||
}
|
||||
|
||||
payload := append(hdr, chunk...)
|
||||
padding := make([]byte, reportLen-uint16(len(payload)))
|
||||
payload = append(payload, padding...)
|
||||
|
||||
if err := d.dev.SetOutputReport(2, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
start = end
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type KeyEvent struct {
|
||||
Key int
|
||||
Pressed bool
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
func (d *Device) ReadKeys(ch chan<- KeyEvent) error {
|
||||
states := make([]byte, keyCount)
|
||||
for {
|
||||
_, buf, err := d.dev.GetInputReport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if int(keyStart+keyCount) > len(buf) {
|
||||
continue
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
for i := 0; i < keyCount; i++ {
|
||||
st := buf[keyStart+i]
|
||||
if st != states[i] {
|
||||
ch <- KeyEvent{
|
||||
Key: i,
|
||||
Pressed: st > 0,
|
||||
Time: t,
|
||||
}
|
||||
states[i] = st
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func KeyCount() int { return keyCount }
|
||||
func KeyRows() int { return keyRows }
|
||||
func KeyCols() int { return keyCols }
|
||||
90
lib/xtouch/output.go
Normal file
90
lib/xtouch/output.go
Normal 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
|
||||
}
|
||||
230
lib/xtouch/xtouch.go
Normal file
230
lib/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