272 lines
8.1 KiB
Go
272 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitlab.com/gomidi/midi/v2"
|
|
"gitlab.com/gomidi/midi/v2/drivers"
|
|
)
|
|
|
|
// Triggers that occur from MIDI messages received.
|
|
type NoteTrigger struct {
|
|
MatchAllNotes bool `fig:"match_all_notes"`
|
|
Channel uint8 `fig:"channel"`
|
|
Note uint8 `fig:"note"`
|
|
Velocity uint8 `fig:"velocity"`
|
|
MatchAllVelocities bool `fig:"match_all_velocities"`
|
|
MidiInfoInRequest bool `fig:"midi_info_in_request"`
|
|
InsecureSkipVerify bool `fig:"insecure_skip_verify"`
|
|
URL string `fig:"url"`
|
|
Method string `fig:"method"`
|
|
Body string `fig:"body"`
|
|
Headers http.Header `fig:"headers"`
|
|
DebugRequest bool `fig:"debug_request"`
|
|
}
|
|
|
|
// Triggers that occur from HTTP messsages received.
|
|
type RequestTrigger struct {
|
|
Channel uint8 `fig:"channel"`
|
|
Note uint8 `fig:"note"`
|
|
Velocity uint8 `fig:"velocity"`
|
|
MidiInfoInRequest bool `fig:"midi_info_in_request"`
|
|
URI string `fig:"uri"`
|
|
}
|
|
|
|
// A common router for both receiving and sending MIDI messages.
|
|
type MidiRouter struct {
|
|
Name string `fig:"name"`
|
|
Device string `fig:"device"`
|
|
DebugListener bool `fig:"debug_listener"`
|
|
DisableListener bool `fig:"disable_listener"`
|
|
NoteTriggers []NoteTrigger `fig:"note_triggers"`
|
|
RequestTriggers []RequestTrigger `fig:"request_triggers"`
|
|
|
|
MidiOut drivers.Out `fig:"-"`
|
|
ListenerStop func() `fig:"-"`
|
|
}
|
|
|
|
// When a MIDI message occurs, send the HTTP request.
|
|
func (r *MidiRouter) sendRequest(channel, note, velocity uint8) {
|
|
// Check each trigger to find requests that match this message.
|
|
for _, trig := range r.NoteTriggers {
|
|
// If match all notes, process this request.
|
|
// If not, check if channel, note, and velocity matches.
|
|
// The velocity may be defined to accept all.
|
|
if trig.MatchAllNotes || (trig.Channel == channel && trig.Note == note && (trig.Velocity == velocity || trig.MatchAllVelocities)) {
|
|
// For all logging, we want to print the message so setup a common string to print.
|
|
logInfo := fmt.Sprintf("note %s(%d) on channel %v with velocity %v", midi.Note(note), note, channel, velocity)
|
|
|
|
// Default method to GET if nothing is defined.
|
|
if trig.Method == "" {
|
|
trig.Method = "GET"
|
|
}
|
|
|
|
// Parse the URL to make sure its valid.
|
|
url, err := url.Parse(trig.URL)
|
|
// If not valid, we need to stop processing this request.
|
|
if err != nil {
|
|
log.Printf("Trigger failed to parse url: %s\n %s\n", err, logInfo)
|
|
continue
|
|
}
|
|
|
|
// If MIDI info needs to be added to the request, add it.
|
|
if trig.MidiInfoInRequest {
|
|
query := url.Query()
|
|
query.Add("channel", strconv.Itoa(int(channel)))
|
|
query.Add("note", strconv.Itoa(int(note)))
|
|
query.Add("velocity", strconv.Itoa(int(velocity)))
|
|
url.RawQuery = query.Encode()
|
|
}
|
|
|
|
// If body provided, setup a reader for it.
|
|
var body io.Reader
|
|
if trig.Body != "" {
|
|
body = strings.NewReader(trig.Body)
|
|
}
|
|
|
|
// If debugging, log that we're starting a request.
|
|
if trig.DebugRequest {
|
|
log.Printf("Starting request for trigger: %s %s\n%s\n", trig.Method, url.String(), logInfo)
|
|
}
|
|
|
|
// Make the request.
|
|
req, err := http.NewRequest(trig.Method, url.String(), body)
|
|
if err != nil {
|
|
log.Printf("Trigger failed to parse url: %s\n %s\n", err, logInfo)
|
|
continue
|
|
}
|
|
|
|
// Add headers to the request.
|
|
req.Header = trig.Headers
|
|
|
|
// Configure transport with trigger config.
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: trig.InsecureSkipVerify},
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
// Perform the request.
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("Trigger failed to request: %s\n %s\n", err, logInfo)
|
|
continue
|
|
}
|
|
|
|
// Close the body at end of request.
|
|
defer res.Body.Close()
|
|
|
|
// If debug enabled, read the body and log it.
|
|
if trig.DebugRequest {
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("Trigger failed to read body: %s\n %s\n", err, logInfo)
|
|
continue
|
|
}
|
|
log.Printf("Trigger response: %s\n%s\n", logInfo, string(body))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handler for HTTP requests.
|
|
func (m *MidiRouter) Handler(w http.ResponseWriter, r *http.Request) {
|
|
// Check each request trigger for ones that match the request URI.
|
|
for _, t := range m.RequestTriggers {
|
|
// If matches request, process MIDI message.
|
|
if t.URI == r.URL.RawPath {
|
|
// Set default values to those from this trigger.
|
|
channel, note, velocity := t.Channel, t.Note, t.Velocity
|
|
// If MIDI info is in the request query, update to request.
|
|
if t.MidiInfoInRequest {
|
|
query := r.URL.Query()
|
|
// Regex to ensure only numbers are processed.
|
|
numRx := regexp.MustCompile(`^[0-9]+$`)
|
|
|
|
// Check for channel, and only configure if request has a valid value.
|
|
ch := query.Get("channel")
|
|
if numRx.MatchString(ch) {
|
|
i, err := strconv.Atoi(ch)
|
|
if err != nil && i <= 255 && i >= 0 {
|
|
channel = uint8(i)
|
|
}
|
|
}
|
|
// Check for note, and only configure if request has a valid value.
|
|
key := query.Get("note")
|
|
if numRx.MatchString(key) {
|
|
i, err := strconv.Atoi(key)
|
|
if err != nil && i < 255 && i >= 0 {
|
|
note = uint8(i)
|
|
}
|
|
}
|
|
// Check for velocity, and only configure if request has a valid value.
|
|
vel := query.Get("velocity")
|
|
if numRx.MatchString(vel) {
|
|
i, err := strconv.Atoi(vel)
|
|
if err != nil && i < 128 && i >= 0 {
|
|
velocity = uint8(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get send function for output.
|
|
send, err := midi.SendTo(m.MidiOut)
|
|
if err != nil {
|
|
log.Printf("Failed to get midi sender for request: %s\n%s\n", t.URI, err)
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Make the MIDI message based on information.
|
|
msg := midi.NoteOn(channel, note, velocity)
|
|
if velocity == 0 {
|
|
msg = midi.NoteOff(channel, note)
|
|
}
|
|
|
|
// Send MIDI message.
|
|
err = send(msg)
|
|
if err != nil {
|
|
log.Printf("Failed to send midi message: %s\n%s\n", t.URI, err)
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update HTTP status to no content as an success message.
|
|
http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Connect to MIDI devices and start listening.
|
|
func (r *MidiRouter) Connect() {
|
|
// If request triggers defined, find the out port.
|
|
if len(r.RequestTriggers) != 0 {
|
|
out, err := midi.FindOutPort(r.Device)
|
|
if err != nil {
|
|
log.Println("Can't find output device:", r.Device)
|
|
} else {
|
|
r.MidiOut = out
|
|
}
|
|
}
|
|
// If listener is disabled, stop here.
|
|
if r.DisableListener {
|
|
return
|
|
}
|
|
|
|
// Try finding input port.
|
|
log.Println("Connecting to device:", r.Device)
|
|
in, err := midi.FindInPort(r.Device)
|
|
if err != nil {
|
|
log.Println("Can't find device:", r.Device)
|
|
return
|
|
}
|
|
|
|
// Start listening to MIDI messages.
|
|
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
|
|
var channel, note, velocity uint8
|
|
switch {
|
|
// Get notes with an velocity set.
|
|
case msg.GetNoteStart(&channel, ¬e, &velocity):
|
|
// If debug, log.
|
|
if r.DebugListener {
|
|
log.Printf("starting note %s(%d) on channel %v with velocity %v\n", midi.Note(note), note, channel, velocity)
|
|
}
|
|
// Process request.
|
|
r.sendRequest(channel, note, velocity)
|
|
|
|
// If no velocity is set, an note end message is received.
|
|
case msg.GetNoteEnd(&channel, ¬e):
|
|
// If debug, log.
|
|
if r.DebugListener {
|
|
log.Printf("ending note %s(%d) on channel %v\n", midi.Note(note), note, channel)
|
|
}
|
|
// Process request.
|
|
r.sendRequest(channel, note, 0)
|
|
default:
|
|
// ignore
|
|
}
|
|
})
|
|
if err != nil {
|
|
log.Printf("Error listening to device: %s\n", err)
|
|
return
|
|
}
|
|
|
|
// Update stop function for disconnects.
|
|
r.ListenerStop = stop
|
|
}
|
|
|
|
// On disconnect, stop and remove output device.
|
|
func (r *MidiRouter) Disconnect() {
|
|
r.MidiOut = nil
|
|
if r.ListenerStop != nil {
|
|
r.ListenerStop()
|
|
}
|
|
}
|