First commit

This commit is contained in:
James Coleman 2023-09-09 20:37:39 -05:00
commit 24f09575ac
10 changed files with 802 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
config.yaml
midi-request-trigger

19
LICENSE Normal file
View File

@ -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.

178
README.md Normal file
View File

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.mrgeckosmedia.midi-request-trigger</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/bin/midi-request-trigger</string>
<string>-c</string>
<string>/path/to/config.yaml</string>
</array>
<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>RunAtLoad</key>
<true/>
<key>OnDemand</key>
<false/>
</dict>
</plist>
```
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
```

85
config.go Normal file
View File

@ -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
}

51
flags.go Normal file
View File

@ -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)
}
}

17
go.mod Normal file
View File

@ -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
)

18
go.sum Normal file
View File

@ -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=

82
http.go Normal file
View File

@ -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)
}
}

79
main.go Normal file
View File

@ -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()
}
}

271
midiRouter.go Normal file
View File

@ -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, &note, &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, &note):
// 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()
}
}