commit 24f09575ac9056b6abfaaba14c1807cb2032edd4 Author: James Coleman Date: Sat Sep 9 20:37:39 2023 -0500 First commit 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() + } +}