Compare commits
11 Commits
fb4e3db827
...
develop
Author | SHA1 | Date | |
---|---|---|---|
0be958c8d1 | |||
14c7fa8f15 | |||
17e9084907 | |||
ae28a4d385 | |||
ce0f466a0e | |||
ff859d6bf7 | |||
8a7254a467 | |||
17361e888e | |||
123b49d603 | |||
09e7c36571 | |||
15a37f3e8c |
4
LICENSE
4
LICENSE
@@ -208,7 +208,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
gpl-test
|
midi-hid
|
||||||
Copyright (C) 2025 datalore
|
Copyright (C) 2025 datalore
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||||||
|
|
||||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
gpl-test Copyright (C) 2025 datalore
|
midi-hid Copyright (C) 2025 datalore
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
36
README.md
36
README.md
@@ -7,18 +7,44 @@ This software allows mapping and translating of MIDI commands to HID inputs on L
|
|||||||
Install with
|
Install with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install git.datalore.sh/datalore/midi-hid@latest
|
go install github.com/d4t4l0r3/midi-hid@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
and run it with `midi-hid`. It reads `config.yaml` from its current working directory, creates the configured virtual gamepads and translates the inputs until SIGINT is received.
|
and run it with `midi-hid`. If no config file is specified, it reads `~/.config/midi-hid/config.yaml`.
|
||||||
|
Every configured midi controller will be represented by a virtual gamepad, and the inputs will be translated until SIGINT is received.
|
||||||
|
|
||||||
See the provided example config on how to configure your controller, it should be pretty self-explanatory.
|
## Configuration
|
||||||
|
|
||||||
## Known issues
|
See `example-config.yaml` For an annotated configuration file.
|
||||||
|
The valid names for buttons are:
|
||||||
|
|
||||||
The midi library used seems to recognise NoteOff messages as NoteOn messages. However, they can still be recognised by checking the velocity, which is always 0 in NoteOff messages. A workaround has been implemented.
|
| Button name | Description |
|
||||||
|
| ------------ | -------------------------------- |
|
||||||
|
| `north` | E.g. Y on XBox or triangle on PS |
|
||||||
|
| `east` | E.g. B on XBox or circle on PS |
|
||||||
|
| `south` | E.g. A on XBox or X on PS |
|
||||||
|
| `west` | E.g. X on XBox or square on PS |
|
||||||
|
| `l1` | Left bumper |
|
||||||
|
| `l2` | Left trigger |
|
||||||
|
| `l3` | Left stick pressed down |
|
||||||
|
| `r1` | Right bumper |
|
||||||
|
| `r2` | Right trigger |
|
||||||
|
| `r3` | Right stick pressed down |
|
||||||
|
| `select` | Select, or Back on XBox |
|
||||||
|
| `start` | Start button |
|
||||||
|
| `dpad-up` | Directional pad up |
|
||||||
|
| `dpad-down` | Directional pad down |
|
||||||
|
| `dpad-left` | Directional pad left |
|
||||||
|
| `dpad-right` | Directional pad right |
|
||||||
|
|
||||||
|
Valid axis are `left-x`, `left-y`, `right-x` and `right-y`.
|
||||||
|
|
||||||
|
### Finding the MIDI channel and note / controller
|
||||||
|
|
||||||
|
Many vendors provide official documentation on the MIDI commands sent by their devices, but if you are unable to find them, you can use `aseqdump -p <port>` to print all MIDI messages sent by your device. Simply interact with the controls you want to map and you will see the corresponding messages.
|
||||||
|
|
||||||
## Third-party libraries
|
## Third-party libraries
|
||||||
|
|
||||||
- <https://github.com/bendahl/uinput>
|
- <https://github.com/bendahl/uinput>
|
||||||
- <https://gitlab.com/gomidi/midi>
|
- <https://gitlab.com/gomidi/midi>
|
||||||
|
- <https://github.com/charmbracelet/log>
|
||||||
|
148
config.go
148
config.go
@@ -1,148 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/goccy/go-yaml"
|
|
||||||
"github.com/bendahl/uinput"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Controller []ControllerConfig `yaml:"controller"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ControllerConfig struct {
|
|
||||||
PortName string `yaml:"portName"`
|
|
||||||
VendorID uint16 `yaml:"vendorID"`
|
|
||||||
ProductID uint16 `yaml:"productID"`
|
|
||||||
Mappings []MappingConfig `yaml:"mappings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MappingConfig struct {
|
|
||||||
Comment string `yaml:"comment"`
|
|
||||||
Type MappingType `yaml:"type"`
|
|
||||||
MidiChannel uint8 `yaml:"midiChannel"`
|
|
||||||
MidiKey uint8 `yaml:"midiKey"`
|
|
||||||
MidiController uint8 `yaml:"midiController"`
|
|
||||||
Button ButtonName `yaml:"button"`
|
|
||||||
Axis AxisName `yaml:"axis"`
|
|
||||||
IsSigned bool `yaml:"isSigned"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MappingType string
|
|
||||||
type ButtonName string
|
|
||||||
type AxisName string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ButtonMappingType MappingType = "button"
|
|
||||||
ControlMappingType MappingType = "control"
|
|
||||||
ButtonNorth ButtonName = "north"
|
|
||||||
ButtonEast ButtonName = "east"
|
|
||||||
ButtonSouth ButtonName = "south"
|
|
||||||
ButtonWest ButtonName = "west"
|
|
||||||
AxisLeftX AxisName = "left-x"
|
|
||||||
AxisLeftY AxisName = "left-y"
|
|
||||||
AxisRightX AxisName = "right-x"
|
|
||||||
AxisRightY AxisName = "right-y"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseConfig(path string) (Config, error) {
|
|
||||||
var config Config
|
|
||||||
|
|
||||||
buffer, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return config, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = yaml.Unmarshal(buffer, &config)
|
|
||||||
if err != nil {
|
|
||||||
return config, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config Config) Construct() (ControllerList, error) {
|
|
||||||
var controllerList ControllerList
|
|
||||||
|
|
||||||
for _, controllerConfig := range config.Controller {
|
|
||||||
actualController, err := controllerConfig.Construct()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
controllerList = append(controllerList, actualController)
|
|
||||||
}
|
|
||||||
|
|
||||||
return controllerList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cc ControllerConfig) Construct() (*Controller, error) {
|
|
||||||
actualController, err := NewController(cc.PortName, cc.VendorID, cc.ProductID)
|
|
||||||
if err != nil {
|
|
||||||
return actualController, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mappingConfig := range cc.Mappings {
|
|
||||||
actualMapping, err := mappingConfig.Construct()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actualController.AddMapping(actualMapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
return actualController, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc MappingConfig) Construct() (Mapping, error) {
|
|
||||||
switch mc.Type {
|
|
||||||
case ButtonMappingType:
|
|
||||||
button, err := mc.Button.Construct()
|
|
||||||
if err != nil {
|
|
||||||
return ButtonMapping{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ButtonMapping{mc.Comment, mc.MidiChannel, mc.MidiKey, button}, nil
|
|
||||||
case ControlMappingType:
|
|
||||||
axis, err := mc.Axis.Construct()
|
|
||||||
if err != nil {
|
|
||||||
return ControlMapping{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ControlMapping{mc.Comment, mc.MidiChannel, mc.MidiController, axis, mc.IsSigned}, nil
|
|
||||||
default:
|
|
||||||
return ButtonMapping{}, fmt.Errorf("Invalid mapping type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bn ButtonName) Construct() (int, error) {
|
|
||||||
switch bn {
|
|
||||||
case ButtonNorth:
|
|
||||||
return uinput.ButtonNorth, nil
|
|
||||||
case ButtonEast:
|
|
||||||
return uinput.ButtonEast, nil
|
|
||||||
case ButtonSouth:
|
|
||||||
return uinput.ButtonSouth, nil
|
|
||||||
case ButtonWest:
|
|
||||||
return uinput.ButtonWest, nil
|
|
||||||
default:
|
|
||||||
return -1, fmt.Errorf("Invalid button name \"%s\"", bn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (an AxisName) Construct() (ControllerAxis, error) {
|
|
||||||
switch an {
|
|
||||||
case AxisLeftX:
|
|
||||||
return LeftX, nil
|
|
||||||
case AxisLeftY:
|
|
||||||
return LeftY, nil
|
|
||||||
case AxisRightX:
|
|
||||||
return RightX, nil
|
|
||||||
case AxisRightY:
|
|
||||||
return RightY, nil
|
|
||||||
default:
|
|
||||||
return -1, fmt.Errorf("Invalid axis name \"%s\"", an)
|
|
||||||
}
|
|
||||||
}
|
|
47
config.yaml
47
config.yaml
@@ -1,47 +0,0 @@
|
|||||||
controller:
|
|
||||||
- portName: DJControl Inpulse 500 MIDI 1
|
|
||||||
vendorID: 0x45e
|
|
||||||
productID: 0x285
|
|
||||||
mappings:
|
|
||||||
- comment: Play left
|
|
||||||
type: button
|
|
||||||
midiChannel: 1
|
|
||||||
midiKey: 7
|
|
||||||
button: west
|
|
||||||
- comment: Hotcue 1 left
|
|
||||||
type: button
|
|
||||||
midiChannel: 6
|
|
||||||
midiKey: 0
|
|
||||||
button: north
|
|
||||||
- comment: Hotcue 5 left
|
|
||||||
type: button
|
|
||||||
midiChannel: 6
|
|
||||||
midiKey: 4
|
|
||||||
button: south
|
|
||||||
- comment: Play right
|
|
||||||
type: button
|
|
||||||
midiChannel: 2
|
|
||||||
midiKey: 7
|
|
||||||
button: east
|
|
||||||
- comment: Volume left
|
|
||||||
type: control
|
|
||||||
midiChannel: 1
|
|
||||||
midiController: 0
|
|
||||||
axis: left-y
|
|
||||||
- comment: Volume right
|
|
||||||
type: control
|
|
||||||
midiChannel: 2
|
|
||||||
midiController: 0
|
|
||||||
axis: right-y
|
|
||||||
- comment: Filter left
|
|
||||||
type: control
|
|
||||||
midiChannel: 1
|
|
||||||
midiController: 1
|
|
||||||
axis: left-x
|
|
||||||
isSigned: true
|
|
||||||
- comment: Filter right
|
|
||||||
type: control
|
|
||||||
midiChannel: 2
|
|
||||||
midiController: 1
|
|
||||||
axis: right-x
|
|
||||||
isSigned: true
|
|
207
config/config.go
Normal file
207
config/config.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/d4t4l0r3/midi-hid/translation"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/bendahl/uinput"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is the root type of a config, consisting of an arbitrary number of controller configs.
|
||||||
|
type Config struct {
|
||||||
|
Controller []ControllerConfig `yaml:"controller"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ControllerConfig represents the data needed to later construct a Controller object.
|
||||||
|
type ControllerConfig struct {
|
||||||
|
PortName string `yaml:"portName"`
|
||||||
|
VendorID uint16 `yaml:"vendorID"`
|
||||||
|
ProductID uint16 `yaml:"productID"`
|
||||||
|
Mappings []MappingConfig `yaml:"mappings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// A MappingConfig consists of all data possibly needed to construct a mapping, both button and control.
|
||||||
|
type MappingConfig struct {
|
||||||
|
Comment string `yaml:"comment"`
|
||||||
|
Type MappingType `yaml:"type"`
|
||||||
|
MidiChannel uint8 `yaml:"midiChannel"`
|
||||||
|
MidiKey uint8 `yaml:"midiKey"`
|
||||||
|
MidiController uint8 `yaml:"midiController"`
|
||||||
|
Button ButtonName `yaml:"button"`
|
||||||
|
Axis AxisName `yaml:"axis"`
|
||||||
|
IsSigned bool `yaml:"isSigned"`
|
||||||
|
Deadzone float64 `yaml:"deadzone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MappingType string
|
||||||
|
type ButtonName string
|
||||||
|
type AxisName string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ButtonMappingType MappingType = "button"
|
||||||
|
ControlMappingType MappingType = "control"
|
||||||
|
ButtonNorth ButtonName = "north"
|
||||||
|
ButtonEast ButtonName = "east"
|
||||||
|
ButtonSouth ButtonName = "south"
|
||||||
|
ButtonWest ButtonName = "west"
|
||||||
|
ButtonL1 ButtonName = "l1"
|
||||||
|
ButtonL2 ButtonName = "l2"
|
||||||
|
ButtonL3 ButtonName = "l3"
|
||||||
|
ButtonR1 ButtonName = "r1"
|
||||||
|
ButtonR2 ButtonName = "r2"
|
||||||
|
ButtonR3 ButtonName = "r3"
|
||||||
|
ButtonSelect ButtonName = "select"
|
||||||
|
ButtonStart ButtonName = "start"
|
||||||
|
ButtonDpadUp ButtonName = "dpad-up"
|
||||||
|
ButtonDpadDown ButtonName = "dpad-down"
|
||||||
|
ButtonDpadLeft ButtonName = "dpad-left"
|
||||||
|
ButtonDpadRight ButtonName = "dpad-right"
|
||||||
|
AxisLeftX AxisName = "left-x"
|
||||||
|
AxisLeftY AxisName = "left-y"
|
||||||
|
AxisRightX AxisName = "right-x"
|
||||||
|
AxisRightY AxisName = "right-y"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseConfig takes the path to a config file and returns the parsed Config object,
|
||||||
|
// or an error if thrown.
|
||||||
|
func ParseConfig(path string) (Config, error) {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
buffer, err := os.ReadFile(os.ExpandEnv(path))
|
||||||
|
if err != nil {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(buffer, &config)
|
||||||
|
if err != nil {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct iterates over all ControllerConfigs and constructs the Controller objects.
|
||||||
|
// In case of a failure, it aborts and returns an error.
|
||||||
|
func (config Config) Construct() (translation.ControllerList, error) {
|
||||||
|
var controllerList translation.ControllerList
|
||||||
|
|
||||||
|
for _, controllerConfig := range config.Controller {
|
||||||
|
actualController, err := controllerConfig.Construct()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
controllerList = append(controllerList, actualController)
|
||||||
|
}
|
||||||
|
|
||||||
|
return controllerList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct builds a Controller object and its corresponding mappings.
|
||||||
|
// Aborts and returns an error if the midi port was not found or one of
|
||||||
|
// the Mappings is invalid.
|
||||||
|
func (cc ControllerConfig) Construct() (*translation.Controller, error) {
|
||||||
|
actualController, err := translation.NewController(cc.PortName, cc.VendorID, cc.ProductID)
|
||||||
|
if err != nil {
|
||||||
|
return actualController, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mappingConfig := range cc.Mappings {
|
||||||
|
actualMapping, err := mappingConfig.Construct()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actualController.AddMapping(actualMapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualController, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct builds the Mapping object. Returns an error if config is invalid.
|
||||||
|
func (mc MappingConfig) Construct() (translation.Mapping, error) {
|
||||||
|
switch mc.Type {
|
||||||
|
case ButtonMappingType:
|
||||||
|
button, err := mc.Button.Construct()
|
||||||
|
if err != nil {
|
||||||
|
return translation.ButtonMapping{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Parsed button mapping", "comment", mc.Comment, "midiChannel", mc.MidiChannel, "midiKey", mc.MidiKey, "button", button)
|
||||||
|
|
||||||
|
return translation.ButtonMapping{mc.Comment, mc.MidiChannel, mc.MidiKey, button}, nil
|
||||||
|
case ControlMappingType:
|
||||||
|
axis, err := mc.Axis.Construct()
|
||||||
|
if err != nil {
|
||||||
|
return translation.ControlMapping{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Parsed control mapping", "comment", mc.Comment, "midiChannel", mc.MidiChannel, "midiController", mc.MidiController, "axis", axis, "isSigned", mc.IsSigned, "deadzone", mc.Deadzone)
|
||||||
|
|
||||||
|
return translation.ControlMapping{mc.Comment, mc.MidiChannel, mc.MidiController, axis, mc.IsSigned, mc.Deadzone}, nil
|
||||||
|
default:
|
||||||
|
return translation.ButtonMapping{}, fmt.Errorf("Invalid mapping type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct converts a ButtonName to its corresponding key code, or returns an error if the
|
||||||
|
// name is unknown.
|
||||||
|
func (bn ButtonName) Construct() (int, error) {
|
||||||
|
switch bn {
|
||||||
|
case ButtonNorth:
|
||||||
|
return uinput.ButtonNorth, nil
|
||||||
|
case ButtonEast:
|
||||||
|
return uinput.ButtonEast, nil
|
||||||
|
case ButtonSouth:
|
||||||
|
return uinput.ButtonSouth, nil
|
||||||
|
case ButtonWest:
|
||||||
|
return uinput.ButtonWest, nil
|
||||||
|
case ButtonL1:
|
||||||
|
return uinput.ButtonBumperLeft, nil
|
||||||
|
case ButtonL2:
|
||||||
|
return uinput.ButtonTriggerLeft, nil
|
||||||
|
case ButtonL3:
|
||||||
|
return uinput.ButtonThumbLeft, nil
|
||||||
|
case ButtonR1:
|
||||||
|
return uinput.ButtonBumperRight, nil
|
||||||
|
case ButtonR2:
|
||||||
|
return uinput.ButtonTriggerRight, nil
|
||||||
|
case ButtonR3:
|
||||||
|
return uinput.ButtonThumbRight, nil
|
||||||
|
case ButtonSelect:
|
||||||
|
return uinput.ButtonSelect, nil
|
||||||
|
case ButtonStart:
|
||||||
|
return uinput.ButtonStart, nil
|
||||||
|
case ButtonDpadUp:
|
||||||
|
return uinput.ButtonDpadUp, nil
|
||||||
|
case ButtonDpadDown:
|
||||||
|
return uinput.ButtonDpadDown, nil
|
||||||
|
case ButtonDpadLeft:
|
||||||
|
return uinput.ButtonDpadLeft, nil
|
||||||
|
case ButtonDpadRight:
|
||||||
|
return uinput.ButtonDpadRight, nil
|
||||||
|
default:
|
||||||
|
return -1, fmt.Errorf("Invalid button name \"%s\"", bn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct converts an AxisName into the internal representation for a ControllerAxis.
|
||||||
|
// Returns an error if AxisName is invalid.
|
||||||
|
func (an AxisName) Construct() (translation.ControllerAxis, error) {
|
||||||
|
switch an {
|
||||||
|
case AxisLeftX:
|
||||||
|
return translation.LeftX, nil
|
||||||
|
case AxisLeftY:
|
||||||
|
return translation.LeftY, nil
|
||||||
|
case AxisRightX:
|
||||||
|
return translation.RightX, nil
|
||||||
|
case AxisRightY:
|
||||||
|
return translation.RightY, nil
|
||||||
|
default:
|
||||||
|
return -1, fmt.Errorf("Invalid axis name \"%s\"", an)
|
||||||
|
}
|
||||||
|
}
|
22
example-config.yaml
Normal file
22
example-config.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
controller:
|
||||||
|
- portName: DJControl Inpulse 500 MIDI 1 # name of the MIDI port to read from. You can find it with aseqdump -l
|
||||||
|
vendorID: 0x45e # vendor ID for the virtual gamepad. Default is 0x45e. See https://gist.github.com/nondebug/aec93dff7f0f1969f4cc2291b24a3171
|
||||||
|
productID: 0x285 # product ID for the virtual gamepad. Default is 0x285. See https://gist.github.com/nondebug/aec93dff7f0f1969f4cc2291b24a3171
|
||||||
|
mappings:
|
||||||
|
- comment: Play left # This is optional, but it can increase comprehensibility. Will also be used in debug output.
|
||||||
|
type: button # Either button or control. A button can be pressed or released, a control represents a range of values.
|
||||||
|
midiChannel: 1 # MIDI channel to listen to
|
||||||
|
midiKey: 7 # MIDI note representing the button
|
||||||
|
button: west # Name of the gamepad button this should be mapped to. For valid names, see README.md
|
||||||
|
- comment: Filter left
|
||||||
|
type: control
|
||||||
|
midiChannel: 1
|
||||||
|
midiController: 1 # MIDI controller representing the physical control
|
||||||
|
axis: left-x # Axis on the virtual gamepad this should be mapped to. For valid names, see README.md
|
||||||
|
isSigned: true # Whether the controller axis should range from -1 to 1 or from 0 to 1. Default is false
|
||||||
|
deadzone: 0.01 # Number between 0 and 1. In this case, all values between -0.01 and 0.01 will be clamped to 0. Default is 0.0
|
||||||
|
- comment: Volume left
|
||||||
|
type: control
|
||||||
|
midiChannel: 1
|
||||||
|
midiController: 0
|
||||||
|
axis: left-y
|
21
go.mod
21
go.mod
@@ -1,9 +1,28 @@
|
|||||||
module git.datalore.sh/datalore/midi-hid
|
module github.com/d4t4l0r3/midi-hid
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bendahl/uinput v1.7.0
|
github.com/bendahl/uinput v1.7.0
|
||||||
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
gitlab.com/gomidi/midi/v2 v2.3.16
|
gitlab.com/gomidi/midi/v2 v2.3.16
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
)
|
||||||
|
42
go.sum
42
go.sum
@@ -1,6 +1,48 @@
|
|||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
|
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
|
||||||
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
|
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
gitlab.com/gomidi/midi/v2 v2.3.16 h1:yufWSENyjnJ4LFQa9BerzUm4E4aLfTyzw5nmnCteO0c=
|
gitlab.com/gomidi/midi/v2 v2.3.16 h1:yufWSENyjnJ4LFQa9BerzUm4E4aLfTyzw5nmnCteO0c=
|
||||||
gitlab.com/gomidi/midi/v2 v2.3.16/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw=
|
gitlab.com/gomidi/midi/v2 v2.3.16/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
24
main.go
24
main.go
@@ -1,10 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
|
"github.com/d4t4l0r3/midi-hid/config"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
"gitlab.com/gomidi/midi/v2"
|
"gitlab.com/gomidi/midi/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,9 +22,22 @@ func must[T any](obj T, err error) T {
|
|||||||
func main() {
|
func main() {
|
||||||
defer midi.CloseDriver()
|
defer midi.CloseDriver()
|
||||||
|
|
||||||
log.Println("Starting...")
|
var (
|
||||||
config := must(ParseConfig("config.yaml"))
|
configPath string
|
||||||
controllerList := must(config.Construct())
|
printDebugMsgs bool
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(&configPath, "f", "$HOME/.config/midi-hid/config.yaml", "Config file")
|
||||||
|
flag.BoolVar(&printDebugMsgs, "debug", false, "Print debug messages")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if printDebugMsgs {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Starting...")
|
||||||
|
conf := must(config.ParseConfig(configPath))
|
||||||
|
controllerList := must(conf.Construct())
|
||||||
defer controllerList.Stop()
|
defer controllerList.Stop()
|
||||||
|
|
||||||
// wait for SIGINT
|
// wait for SIGINT
|
||||||
|
123
mapping.go
123
mapping.go
@@ -1,123 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"gitlab.com/gomidi/midi/v2"
|
|
||||||
"github.com/bendahl/uinput"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Mapping interface {
|
|
||||||
Is(midi.Message) bool
|
|
||||||
TriggerIfMatch(midi.Message, uinput.Gamepad) error
|
|
||||||
Comment() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ButtonMapping struct {
|
|
||||||
comment string
|
|
||||||
midiChannel uint8
|
|
||||||
midiKey uint8
|
|
||||||
gamepadKey int
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Printf("%s: Button down\n", m.comment)
|
|
||||||
return virtGamepad.ButtonDown(m.gamepadKey)
|
|
||||||
}
|
|
||||||
fallthrough // if reached here, velocity is 0 -> NoteOff
|
|
||||||
case midi.NoteOffMsg:
|
|
||||||
log.Printf("%s: Button up\n", m.comment)
|
|
||||||
return virtGamepad.ButtonUp(m.gamepadKey)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Invalid message type triggered ButtonMapping")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m ButtonMapping) Comment() string {
|
|
||||||
return m.comment
|
|
||||||
}
|
|
||||||
|
|
||||||
type ControllerAxis int
|
|
||||||
|
|
||||||
const (
|
|
||||||
LeftX ControllerAxis = iota
|
|
||||||
LeftY
|
|
||||||
RightX
|
|
||||||
RightY
|
|
||||||
)
|
|
||||||
|
|
||||||
type ControlMapping struct {
|
|
||||||
comment string
|
|
||||||
midiChannel uint8
|
|
||||||
midiController uint8
|
|
||||||
axis ControllerAxis
|
|
||||||
isSigned bool
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m ControlMapping) TriggerIfMatch(msg midi.Message, virtGamepad uinput.Gamepad) error {
|
|
||||||
if m.Is(msg) {
|
|
||||||
var (
|
|
||||||
valueAbsolute uint8
|
|
||||||
valueNormalised float32
|
|
||||||
)
|
|
||||||
|
|
||||||
msg.GetControlChange(nil, nil, &valueAbsolute)
|
|
||||||
|
|
||||||
// value is 0-127, normalise
|
|
||||||
valueNormalised = float32(valueAbsolute) / 127
|
|
||||||
if m.isSigned {
|
|
||||||
valueNormalised *= 2
|
|
||||||
valueNormalised -= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("%s: value %v\n", m.comment, valueNormalised)
|
|
||||||
|
|
||||||
switch m.axis {
|
|
||||||
case LeftX:
|
|
||||||
return virtGamepad.LeftStickMoveX(valueNormalised)
|
|
||||||
case LeftY:
|
|
||||||
return virtGamepad.LeftStickMoveY(valueNormalised)
|
|
||||||
case RightX:
|
|
||||||
return virtGamepad.RightStickMoveX(valueNormalised)
|
|
||||||
case RightY:
|
|
||||||
return virtGamepad.RightStickMoveY(valueNormalised)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m ControlMapping) Comment() string {
|
|
||||||
return m.comment
|
|
||||||
}
|
|
@@ -1,20 +1,23 @@
|
|||||||
package main
|
package translation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"github.com/charmbracelet/log"
|
||||||
|
|
||||||
"gitlab.com/gomidi/midi/v2"
|
"gitlab.com/gomidi/midi/v2"
|
||||||
"github.com/bendahl/uinput"
|
"github.com/bendahl/uinput"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// A ControllerList is a list of controllers. Duh.
|
||||||
type ControllerList []*Controller
|
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() {
|
func (cl ControllerList) Stop() {
|
||||||
for _, controller := range cl {
|
for _, controller := range cl {
|
||||||
controller.Stop()
|
controller.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Controller object manages the translation from MIDI to uinput.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
midiInput *MidiInput
|
midiInput *MidiInput
|
||||||
mappings []Mapping
|
mappings []Mapping
|
||||||
@@ -22,7 +25,14 @@ type Controller struct {
|
|||||||
virtGamepad uinput.Gamepad
|
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) {
|
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)
|
midiInput, err := NewMidiInput(portName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -51,10 +61,12 @@ func NewController(portName string, vendorID, productID uint16) (*Controller, er
|
|||||||
return controller, nil
|
return controller, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddMapping adds a mapping to the Controller.
|
||||||
func (c *Controller) AddMapping(mapping Mapping) {
|
func (c *Controller) AddMapping(mapping Mapping) {
|
||||||
c.mappings = append(c.mappings, mapping)
|
c.mappings = append(c.mappings, mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop quits the update loop and terminates all corresponding connections.
|
||||||
func (c Controller) Stop() {
|
func (c Controller) Stop() {
|
||||||
c.midiInput.Stop()
|
c.midiInput.Stop()
|
||||||
c.abortChan <- struct{}{}
|
c.abortChan <- struct{}{}
|
||||||
@@ -65,7 +77,7 @@ func (c Controller) update(msg midi.Message) {
|
|||||||
for _, mapping := range c.mappings {
|
for _, mapping := range c.mappings {
|
||||||
err := mapping.TriggerIfMatch(msg, c.virtGamepad)
|
err := mapping.TriggerIfMatch(msg, c.virtGamepad)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error in Mapping \"%s\": %v\n", mapping.Comment(), err)
|
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
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package translation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitlab.com/gomidi/midi/v2"
|
"gitlab.com/gomidi/midi/v2"
|
||||||
@@ -6,12 +6,16 @@ import (
|
|||||||
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
_ "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 {
|
type MidiInput struct {
|
||||||
input drivers.In
|
input drivers.In
|
||||||
Messages chan midi.Message
|
Messages chan midi.Message
|
||||||
Stop func()
|
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) {
|
func NewMidiInput(portName string) (*MidiInput, error) {
|
||||||
input, err := midi.FindInPort(portName)
|
input, err := midi.FindInPort(portName)
|
||||||
if err != nil {
|
if err != nil {
|
Reference in New Issue
Block a user