housekeeping: Refactored API to separate packages
This commit is contained in:
83
translation/controller.go
Normal file
83
translation/controller.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package translation
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/log"
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
"github.com/bendahl/uinput"
|
||||
)
|
||||
|
||||
// A ControllerList is a list of controllers. Duh.
|
||||
type ControllerList []*Controller
|
||||
|
||||
// Stop iterates over all Controller objects and Stops their update loops and MIDI connections.
|
||||
// Always call this for a clean shutdown. Meant to be deferred.
|
||||
func (cl ControllerList) Stop() {
|
||||
for _, controller := range cl {
|
||||
controller.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// A Controller object manages the translation from MIDI to uinput.
|
||||
type Controller struct {
|
||||
midiInput *MidiInput
|
||||
mappings []Mapping
|
||||
abortChan chan interface{}
|
||||
virtGamepad uinput.Gamepad
|
||||
}
|
||||
|
||||
// NewController builds a new Controller object reading from the MIDI port specified by portName,
|
||||
// and registers a virtual uinput-Gamepad using vendorID and productID.
|
||||
func NewController(portName string, vendorID, productID uint16) (*Controller, error) {
|
||||
if vendorID == 0 && productID == 0 {
|
||||
// if no IDs were defined, imitate XBox 360 controller
|
||||
vendorID = 0x45e
|
||||
productID = 0x285
|
||||
}
|
||||
midiInput, err := NewMidiInput(portName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
virtGamepad, err := uinput.CreateGamepad("/dev/uinput", []byte(portName), vendorID, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
abortChan := make(chan interface{})
|
||||
|
||||
controller := &Controller{midiInput, nil, abortChan, virtGamepad}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case midiMessage := <-midiInput.Messages:
|
||||
controller.update(midiMessage)
|
||||
case <-abortChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return controller, nil
|
||||
}
|
||||
|
||||
// AddMapping adds a mapping to the Controller.
|
||||
func (c *Controller) AddMapping(mapping Mapping) {
|
||||
c.mappings = append(c.mappings, mapping)
|
||||
}
|
||||
|
||||
// Stop quits the update loop and terminates all corresponding connections.
|
||||
func (c Controller) Stop() {
|
||||
c.midiInput.Stop()
|
||||
c.abortChan <- struct{}{}
|
||||
c.virtGamepad.Close()
|
||||
}
|
||||
|
||||
func (c Controller) update(msg midi.Message) {
|
||||
for _, mapping := range c.mappings {
|
||||
err := mapping.TriggerIfMatch(msg, c.virtGamepad)
|
||||
if err != nil {
|
||||
log.Errorf("Error in Mapping \"%s\": %v", mapping.Comment(), err)
|
||||
}
|
||||
}
|
||||
}
|
139
translation/mapping.go
Normal file
139
translation/mapping.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package translation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
"github.com/bendahl/uinput"
|
||||
)
|
||||
|
||||
// A Mapping is an interface for all types of Mappings.
|
||||
type Mapping interface {
|
||||
Is(midi.Message) bool
|
||||
TriggerIfMatch(midi.Message, uinput.Gamepad) error
|
||||
Comment() string
|
||||
}
|
||||
|
||||
// A ButtonMapping maps a MIDI Note to a gamepad button.
|
||||
type ButtonMapping struct {
|
||||
CommentStr string
|
||||
MidiChannel uint8
|
||||
MidiKey uint8
|
||||
GamepadKey int
|
||||
}
|
||||
|
||||
// Is checks if the MIDI message msg triggers this Mapping, without actually triggering it.
|
||||
func (m ButtonMapping) Is(msg midi.Message) bool {
|
||||
var channel, key uint8
|
||||
|
||||
switch {
|
||||
case msg.GetNoteOn(&channel, &key, nil), msg.GetNoteOff(&channel, &key, nil):
|
||||
return (m.MidiChannel == channel && m.MidiKey == key)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerIfMatch checks if the MIDI message msg triggers this Mapping, and if so,
|
||||
// sends the corresponding input to virtGamepad.
|
||||
func (m ButtonMapping) TriggerIfMatch(msg midi.Message, virtGamepad uinput.Gamepad) error {
|
||||
if m.Is(msg) {
|
||||
var velocity uint8
|
||||
msg.GetNoteOn(nil, nil, &velocity)
|
||||
switch msg.Type() {
|
||||
case midi.NoteOnMsg:
|
||||
if velocity != 0 {
|
||||
log.Debug(m.CommentStr, "status", "down")
|
||||
return virtGamepad.ButtonDown(m.GamepadKey)
|
||||
}
|
||||
fallthrough // if reached here, velocity is 0 -> NoteOff
|
||||
case midi.NoteOffMsg:
|
||||
log.Debug(m.CommentStr, "status", "up")
|
||||
return virtGamepad.ButtonUp(m.GamepadKey)
|
||||
default:
|
||||
return fmt.Errorf("Invalid message type triggered ButtonMapping")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Comment returns the Mappings comment.
|
||||
func (m ButtonMapping) Comment() string {
|
||||
return m.CommentStr
|
||||
}
|
||||
|
||||
type ControllerAxis int
|
||||
|
||||
const (
|
||||
LeftX ControllerAxis = iota
|
||||
LeftY
|
||||
RightX
|
||||
RightY
|
||||
)
|
||||
|
||||
type ControlMapping struct {
|
||||
CommentStr string
|
||||
MidiChannel uint8
|
||||
MidiController uint8
|
||||
Axis ControllerAxis
|
||||
IsSigned bool
|
||||
Deadzone float64
|
||||
}
|
||||
|
||||
// Is checks if the MIDI message msg triggers this Mapping, without actually triggering it.
|
||||
func (m ControlMapping) Is(msg midi.Message) bool {
|
||||
var channel, controller uint8
|
||||
|
||||
if msg.GetControlChange(&channel, &controller, nil) {
|
||||
return (m.MidiChannel == channel && m.MidiController == controller)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerIfMatch checks if the MIDI message msg triggers this Mapping, and if so,
|
||||
// sends the corresponding input to virtGamepad.
|
||||
func (m ControlMapping) TriggerIfMatch(msg midi.Message, virtGamepad uinput.Gamepad) error {
|
||||
if m.Is(msg) {
|
||||
var (
|
||||
valueAbsolute uint8
|
||||
valueNormalised float64
|
||||
)
|
||||
|
||||
msg.GetControlChange(nil, nil, &valueAbsolute)
|
||||
|
||||
// value is 0-127, normalise
|
||||
valueNormalised = float64(valueAbsolute) / 127
|
||||
if m.IsSigned {
|
||||
valueNormalised *= 2
|
||||
valueNormalised -= 1
|
||||
}
|
||||
|
||||
if math.Abs(valueNormalised) < m.Deadzone {
|
||||
valueNormalised = 0
|
||||
}
|
||||
|
||||
log.Debug(m.CommentStr, "value", valueNormalised, "deadzone", m.Deadzone)
|
||||
|
||||
switch m.Axis {
|
||||
case LeftX:
|
||||
return virtGamepad.LeftStickMoveX(float32(valueNormalised))
|
||||
case LeftY:
|
||||
return virtGamepad.LeftStickMoveY(float32(valueNormalised))
|
||||
case RightX:
|
||||
return virtGamepad.RightStickMoveX(float32(valueNormalised))
|
||||
case RightY:
|
||||
return virtGamepad.RightStickMoveY(float32(valueNormalised))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Comment returns the Mappings comment.
|
||||
func (m ControlMapping) Comment() string {
|
||||
return m.CommentStr
|
||||
}
|
39
translation/midi.go
Normal file
39
translation/midi.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package translation
|
||||
|
||||
import (
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
"gitlab.com/gomidi/midi/v2/drivers"
|
||||
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
||||
)
|
||||
|
||||
// A MidiInput represents a MIDI input and provides a channel to read incoming
|
||||
// Messages, as well as a Stop function to terminate the connection.
|
||||
type MidiInput struct {
|
||||
input drivers.In
|
||||
Messages chan midi.Message
|
||||
Stop func()
|
||||
}
|
||||
|
||||
// NewMidiInput initialises a MidiInput object connected to the MIDI port specified
|
||||
// by portName. Retuns an error if the connection fails.
|
||||
func NewMidiInput(portName string) (*MidiInput, error) {
|
||||
input, err := midi.FindInPort(portName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages := make(chan midi.Message)
|
||||
|
||||
midiListener := func(msg midi.Message, timestampMs int32) {
|
||||
if msg.IsOneOf(midi.NoteOnMsg, midi.NoteOffMsg, midi.ControlChangeMsg) {
|
||||
messages <- msg
|
||||
}
|
||||
}
|
||||
|
||||
stopFunc, err := midi.ListenTo(input, midiListener, midi.UseSysEx())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MidiInput{input, messages, stopFunc}, nil
|
||||
}
|
Reference in New Issue
Block a user