First commit
This commit is contained in:
commit
ad5e3a6ff8
27
.github/workflows/release.yaml
vendored
Normal file
27
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
releases-matrix:
|
||||||
|
name: Release Go Binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
goos: [linux, darwin]
|
||||||
|
goarch: [amd64, arm64]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: wangyoucao577/go-release-action@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
goos: ${{ matrix.goos }}
|
||||||
|
goarch: ${{ matrix.goarch }}
|
||||||
|
goversion: "https://dl.google.com/go/go1.20.5.linux-amd64.tar.gz"
|
||||||
|
project_path: "./"
|
||||||
|
binary_name: "midi-request-trigger"
|
||||||
|
extra_files: LICENSE README.md
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
config.yaml
|
||||||
|
midi-request-trigger
|
19
LICENSE
Normal file
19
LICENSE
Normal 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
178
README.md
Normal 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 either by downloading the latest binary release or 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 IPA 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
85
config.go
Normal 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
51
flags.go
Normal 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
17
go.mod
Normal 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
18
go.sum
Normal 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
82
http.go
Normal 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
79
main.go
Normal 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
271
midiRouter.go
Normal 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, ¬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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user