From 24f09575ac9056b6abfaaba14c1807cb2032edd4 Mon Sep 17 00:00:00 2001 From: James Coleman Date: Sat, 9 Sep 2023 20:37:39 -0500 Subject: [PATCH] First commit --- .gitignore | 2 + LICENSE | 19 ++++ README.md | 178 +++++++++++++++++++++++++++++++++ config.go | 85 ++++++++++++++++ flags.go | 51 ++++++++++ go.mod | 17 ++++ go.sum | 18 ++++ http.go | 82 +++++++++++++++ main.go | 79 +++++++++++++++ midiRouter.go | 271 ++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 802 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.go create mode 100644 flags.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 http.go create mode 100644 main.go create mode 100644 midiRouter.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47d3278 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config.yaml +midi-request-trigger \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c9e3c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..11441e7 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# midi-request-trigger + +A service that triggers HTTP requests when MIDI messages are recieved and triggers MIDI messages when HTTP requests are received. + +## Install + +You can install by building. + +### Building + +Building should be as simple as running: + +```bash +go build +``` + +### Running as a service + +You are likely going to want to run the tool as a service to ensure it runs at boot and restarts in case of failures. Below is an example service config file you can place in `/etc/systemd/system/midi-request-trigger.service` on a linux system to run as a service if you install the binary in `/usr/local/bin/`. + +```systemd +[Unit] +Description=MIDI Request Trigger +After=network.target +StartLimitIntervalSec=500 +StartLimitBurst=5 + +[Service] +ExecStart=/usr/local/bin/midi-request-trigger +ExecReload=/bin/kill -s HUP $MAINPID +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target +``` + +Once the service file is installed, you can run the following to start it: + +```bash +systemctl daemon-reload +systemctl start midi-request-trigger.service +``` + +On MacOS, you can setup a Launch Agent in `~/Library/LaunchAgents/com.mrgeckosmedia.midi-request-trigger.plist` as follows: + +```xml + + + + + Label + com.mrgeckosmedia.midi-request-trigger + ProgramArguments + + /path/to/bin/midi-request-trigger + -c + /path/to/config.yaml + + KeepAlive + + Crashed + + SuccessfulExit + + + RunAtLoad + + OnDemand + + + + +``` + +Start with: +```bash +launchctl load ~/Library/LaunchAgents/com.mrgeckosmedia.midi-request-trigger.plist +``` + +Check status with: +```bash +launchctl list com.mrgeckosmedia.midi-request-trigger +``` + +Stop with: +```bash +launchctl unload ~/Library/LaunchAgents/com.mrgeckosmedia.midi-request-trigger.plist +``` + + +## Config + +The default configuration paths are: + +- `./config.yaml` - A file in the current working directory. +- `~/.config/midi-request-trigger/config.yaml` - A file in your home directory's config path. +- `/etc/midi-request-trigger/config.yaml` - A file in the etc config folder. + +### To verify listener works + +You can find the device name by running the following: +```bash +midi-request-trigger -l +``` + +On MacOS, there is an IAC Driver that can be enabled in Audio MIDI Setup. +```yaml +--- +midi_routers: + - name: service_notifications + device: IAC Driver Bus 1 + debug_listener: true +``` + +### Example note trigger configuration + +```yaml +--- +midi_routers: + - name: service_notifications + device: IAC Driver Bus 1 + debug_listener: true + note_triggers: + - channel: 0 + note: 0 + match_all_velocities: true + url: http://example.com + midi_info_in_request: true +``` + +### Example request trigger configuration + +```yaml +--- +midi_routers: + - name: service_notifications + device: IAC Driver Bus 1 + debug_listener: true + request_triggers: + - channel: 0 + note: 0 + velocity: 1 + midi_info_in_request: true + uri: /send_note +``` + +### Example multi part request + +```yaml +--- +midi_routers: + - name: service_notifications + device: IAC Driver Bus 1 + debug_listener: true + note_triggers: + - channel: 0 + note: 0 + match_all_velocities: true + url: http://example.com + method: POST + body: | + -----------------------------888832887744 + Content-Disposition: form-data; name="message" + + example variable + -----------------------------888832887744 + Content-Disposition: form-data; name="file"; filename="example.txt" + Content-Type: text/plain + + Content of file. + + -----------------------------888832887744-- + headers: + Content-Type: + - multipart/form-data; boundary=---------------------------888832887744 + debug_request: true +``` \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..56495d3 --- /dev/null +++ b/config.go @@ -0,0 +1,85 @@ +package main + +import ( + "log" + "os" + "os/user" + "path" + "path/filepath" + + "github.com/kkyr/fig" +) + +// Configurations relating to HTTP server. +type HTTPConfig struct { + BindAddr string `fig:"bind_addr"` + Port uint `fig:"port"` + Debug bool `fig:"debug"` + APIKey string `fig:"api_key"` + Enabled bool `fig:"enabled"` +} + +// Configuration Structure. +type Config struct { + HTTP HTTPConfig `fig:"http"` + MidiRouters []*MidiRouter `fig:"midi_routers"` +} + +// Load the configuration. +func (a *App) ReadConfig() { + usr, err := user.Current() + if err != nil { + log.Fatal(err) + } + + // Configuration paths. + localConfig, _ := filepath.Abs("./config.yaml") + homeDirConfig := usr.HomeDir + "/.config/midi-request-trigger/config.yaml" + etcConfig := "/etc/midi-request-trigger/config.yaml" + + // Determine which configuration to use. + var configFile string + if _, err := os.Stat(app.flags.ConfigPath); err == nil && app.flags.ConfigPath != "" { + configFile = app.flags.ConfigPath + } else if _, err := os.Stat(localConfig); err == nil { + configFile = localConfig + } else if _, err := os.Stat(homeDirConfig); err == nil { + configFile = homeDirConfig + } else if _, err := os.Stat(etcConfig); err == nil { + configFile = etcConfig + } else { + log.Fatal("Unable to find a configuration file.") + } + + // Load the configuration file. + config := &Config{ + HTTP: HTTPConfig{ + BindAddr: "", + Port: 34936, + Debug: true, + Enabled: false, + }, + } + + // Load configuration. + filePath, fileName := path.Split(configFile) + err = fig.Load(config, + fig.File(fileName), + fig.Dirs(filePath), + ) + if err != nil { + log.Printf("Error parsing configuration: %s\n", err) + return + } + + // Flag Overrides. + if app.flags.HTTPBind != "" { + config.HTTP.BindAddr = app.flags.HTTPBind + } + if app.flags.HTTPPort != 0 { + config.HTTP.Port = app.flags.HTTPPort + } + + // Set global config structure. + app.config = config +} diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..67d1964 --- /dev/null +++ b/flags.go @@ -0,0 +1,51 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +// Flags supplied to cli. +type Flags struct { + ConfigPath string + HTTPBind string + HTTPPort uint + ListMidiDevices bool +} + +// Parse the supplied flags. +func (a *App) ParseFlags() { + app.flags = new(Flags) + flag.Usage = func() { + fmt.Printf(serviceName + ": " + serviceDescription + ".\n\nUsage:\n") + flag.PrintDefaults() + } + + // If version is requested. + var printVersion bool + flag.BoolVar(&printVersion, "v", false, "Print version") + + // Override configuration path. + usage := "Load configuration from `FILE`" + flag.StringVar(&app.flags.ConfigPath, "config", "", usage) + flag.StringVar(&app.flags.ConfigPath, "c", "", usage+" (shorthand)") + + // Config overrides for http configurations. + flag.StringVar(&app.flags.HTTPBind, "http-bind", "", "Bind address for http server") + flag.UintVar(&app.flags.HTTPPort, "http-port", 0, "Bind port for http server") + + // Lists available devices. + usage = "List available midi devices for use in configurations" + flag.BoolVar(&app.flags.ListMidiDevices, "list", false, usage) + flag.BoolVar(&app.flags.ListMidiDevices, "l", false, usage+" (shorthand)") + + // Parse the flags. + flag.Parse() + + // Print version and exit if requested. + if printVersion { + fmt.Println(serviceName + ": " + serviceVersion) + os.Exit(0) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4d36dc2 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/GRMrGecko/midi-request-trigger + +go 1.20 + +require ( + github.com/gorilla/handlers v1.5.1 + github.com/gorilla/mux v1.8.0 + github.com/kkyr/fig v0.3.2 + gitlab.com/gomidi/midi/v2 v2.0.30 +) + +require ( + github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/pelletier/go-toml v1.9.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..793acb5 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kkyr/fig v0.3.2 h1:+vMj52FL6RJUxeKOBB6JXIMyyi1/2j1ERDrZXjoBjzM= +github.com/kkyr/fig v0.3.2/go.mod h1:ItUILF8IIzgZOMhx5xpJ1W/bviQsWRKOwKXfE/tqUoA= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +gitlab.com/gomidi/midi/v2 v2.0.30 h1:RgRYbQeQSab5ZaP1lqRcCTnTSBQroE3CE6V9HgMmOAc= +gitlab.com/gomidi/midi/v2 v2.0.30/go.mod h1:Y6IFFyABN415AYsFMPJb0/43TRIuVYDpGKp2gDYLTLI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http.go b/http.go new file mode 100644 index 0000000..8fc1f5b --- /dev/null +++ b/http.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" +) + +// Basic HTTP server structure. +type HTTPServer struct { + server *http.Server + mux *mux.Router + config *HTTPConfig +} + +// This functions starts the HTTP server. +func NewHTTPServer() *HTTPServer { + s := new(HTTPServer) + // Update config reference. + s.config = &app.config.HTTP + s.server = &http.Server{} + s.server.Addr = fmt.Sprintf("%s:%d", s.config.BindAddr, s.config.Port) + + // Setup router. + r := mux.NewRouter() + s.mux = r + // Default to notice of service being online. + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "MIDI Request Trigger is available\n") + }) + + s.server.Handler = r + // If the debug log is enabled, we'll add a middleware handler to log then pass the request to mux router. + if app.config.HTTP.Debug { + s.server.Handler = handlers.CombinedLoggingHandler(os.Stdout, r) + } + + return s +} + +// Start the HTTP server. +func (s *HTTPServer) Start(ctx context.Context) { + isListening := make(chan bool) + // Start server. + go s.StartWithIsListening(ctx, isListening) + // Allow the http server to initialize. + <-isListening +} + +// Starts the HTTP server with a listening channel. +func (s *HTTPServer) StartWithIsListening(ctx context.Context, isListening chan bool) { + // Watch the background context for when we need to shutdown. + go func() { + <-ctx.Done() + err := s.server.Shutdown(context.Background()) + if err != nil { + // Error from closing listeners, or context timeout: + log.Println("Error shutting down http server:", err) + } + }() + + // Start the server. + log.Println("Starting http server:", s.server.Addr) + l, err := net.Listen("tcp", s.server.Addr) + if err != nil { + log.Fatal("Listen: ", err) + } + // Now notify we are listening. + isListening <- true + // Serve http server on the listening port. + err = s.server.Serve(l) + if err != nil { + log.Println("HTTP server failure:", err) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9684f06 --- /dev/null +++ b/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "gitlab.com/gomidi/midi/v2" + _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" +) + +const ( + serviceName = "midi-request-trigger" + serviceDescription = "Takes trigger MIDI messages by HTTP requests and trigger HTTP requests by MIDI messages" + serviceVersion = "0.1" +) + +// App is the global application structure for communicating between servers and storing information. +type App struct { + flags *Flags + config *Config + http *HTTPServer +} + +var app *App + +func main() { + app = new(App) + app.ParseFlags() + app.ReadConfig() + app.http = NewHTTPServer() + + // Make sure midi drivers are closed when the app closes. + defer midi.CloseDriver() + + // If no routers defined, or request to list devices. + if app.flags.ListMidiDevices || len(app.config.MidiRouters) == 0 { + // If no routers are defined, print notice about configuring one. + if len(app.config.MidiRouters) == 0 { + log.Println("No routers configured, please configure one.") + } + // Print available devices. + fmt.Printf("MIDI in ports\n") + fmt.Println(midi.GetInPorts()) + fmt.Printf("\n\nMIDI out ports\n") + fmt.Println(midi.GetOutPorts()) + fmt.Printf("\n\n") + return + } + + // Connect to each router and and setup HTTP handlers. + for _, router := range app.config.MidiRouters { + router.Connect() + for _, trig := range router.RequestTriggers { + app.http.mux.HandleFunc(trig.URI, router.Handler) + } + } + + // Setup context with cancellation function to allow background services to gracefully stop. + ctx, ctxCancel := context.WithCancel(context.Background()) + // Start listening on HTTP server. + app.http.Start(ctx) + + // Monitor common signals. + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + // Wait for a signal. + <-c + // Stop HTTP server. + ctxCancel() + + // Disconnect all MIDI listeners. + for _, router := range app.config.MidiRouters { + router.Disconnect() + } +} diff --git a/midiRouter.go b/midiRouter.go new file mode 100644 index 0000000..7bdc053 --- /dev/null +++ b/midiRouter.go @@ -0,0 +1,271 @@ +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() + } +}