diff --git a/LICENSE b/LICENSE index 270e387..dbfe9bc 100644 --- a/LICENSE +++ b/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. - gpl-test + midi-hid 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. @@ -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: - 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 is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/README.md b/README.md index d75d7f3..bec7ae4 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,44 @@ This software allows mapping and translating of MIDI commands to HID inputs on L Install with ```bash -go install git.datalore.sh/datalore/midi-hid@latest +go install github.com/d4t4l0r3/midi-hid@latest ``` 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 ` 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 - - + - diff --git a/config.yaml b/config.yaml deleted file mode 100644 index a9d3f63..0000000 --- a/config.yaml +++ /dev/null @@ -1,49 +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 - deadzone: 0.01 - - comment: Filter right - type: control - midiChannel: 2 - midiController: 1 - axis: right-x - isSigned: true - deadzone: 0.01 diff --git a/config/config.go b/config/config.go index ade05ab..b5c62d0 100644 --- a/config/config.go +++ b/config/config.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "git.datalore.sh/datalore/midi-hid/translation" + "github.com/d4t4l0r3/midi-hid/translation" "github.com/goccy/go-yaml" "github.com/bendahl/uinput" @@ -32,6 +32,7 @@ type MappingConfig struct { MidiKey uint8 `yaml:"midiKey"` MidiController uint8 `yaml:"midiController"` Button ButtonName `yaml:"button"` + ButtonNegative ButtonName `yaml:"buttonNegative"` Axis AxisName `yaml:"axis"` IsSigned bool `yaml:"isSigned"` Deadzone float64 `yaml:"deadzone"` @@ -44,6 +45,7 @@ type AxisName string const ( ButtonMappingType MappingType = "button" ControlMappingType MappingType = "control" + EncoderMappingType MappingType = "encoder" ButtonNorth ButtonName = "north" ButtonEast ButtonName = "east" ButtonSouth ButtonName = "south" @@ -134,6 +136,20 @@ func (mc MappingConfig) Construct() (translation.Mapping, error) { 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 EncoderMappingType: + button, err := mc.Button.Construct() + if err != nil { + return translation.EncoderMapping{}, err + } + + buttonNegative, err := mc.ButtonNegative.Construct() + if err != nil { + return translation.EncoderMapping{}, err + } + + log.Debug("Parsed encoder mapping", "comment", mc.Comment, "midiChannel", mc.MidiChannel, "midiController", mc.MidiController, "button", button, "buttonNegative", buttonNegative) + + return translation.EncoderMapping{mc.Comment, mc.MidiChannel, mc.MidiController, button, buttonNegative}, nil case ControlMappingType: axis, err := mc.Axis.Construct() if err != nil { diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..661e279 --- /dev/null +++ b/example-config.yaml @@ -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 diff --git a/go.mod b/go.mod index 478140c..3ad605a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.datalore.sh/datalore/midi-hid +module github.com/d4t4l0r3/midi-hid go 1.24.5 diff --git a/main.go b/main.go index bc6fd14..f14c701 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ import ( "os" "os/signal" - "git.datalore.sh/datalore/midi-hid/config" + "github.com/d4t4l0r3/midi-hid/config" "github.com/charmbracelet/log" "gitlab.com/gomidi/midi/v2" diff --git a/translation/mapping.go b/translation/mapping.go index 9867695..28cbd88 100644 --- a/translation/mapping.go +++ b/translation/mapping.go @@ -65,6 +65,53 @@ func (m ButtonMapping) Comment() string { return m.CommentStr } +// An EncoderMapping maps a MIDI Controller to two buttons. +type EncoderMapping struct { + CommentStr string + MidiChannel uint8 + MidiController uint8 + GamepadKeyPositive int + GamepadKeyNegative int +} + +// Is checks if the MIDI message msg triggers this Mapping, without actually triggering it. +func (m EncoderMapping) 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 EncoderMapping) TriggerIfMatch(msg midi.Message, virtGamepad uinput.Gamepad) error { + if m.Is(msg) { + var valueAbsolute uint8 + + msg.GetControlChange(nil, nil, &valueAbsolute) + + switch valueAbsolute { + case 1: + log.Debug(m.CommentStr, "status", "increased") + return virtGamepad.ButtonPress(m.GamepadKeyPositive) + case 127: + log.Debug(m.CommentStr, "status", "decreased") + return virtGamepad.ButtonPress(m.GamepadKeyNegative) + default: + return fmt.Errorf("Invalid message type triggered ButtonMapping") + } + } + + return nil +} + +// Comment returns the Mappings comment. +func (m EncoderMapping) Comment() string { + return m.CommentStr +} type ControllerAxis int const (