summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Terin Stock <terinjokes@gmail.com>2018-07-29 19:37:27 +0000
committerLibravatar Terin Stock <terinjokes@gmail.com>2019-01-27 17:55:12 -0800
commitca933d746ba16ef326d9b4ba85d51940ab9e8add (patch)
tree203e060468f27bb05095aaa1cb755d7c961bd530
parentchore: initial commit (diff)
downloadmidifi-ca933d746ba16ef326d9b4ba85d51940ab9e8add.tar.xz
feat: initial version of midifi software
This CL introduces the initial version of the midifi software, via a simplified CLI interface "playsmf". Though it is far from complete, this version can be manually invoked with the locations of the MIDI communications port (the `-com` flag), and MIDI file. It will playback the MIDI file with the correct time. Software is currently tested with an integration test, that invokes the program with a known MIDI file and compares the output of the serial port, standard out, and standard error. Golden files can be updated by providing the `-test.update-golden` flag. Change-Id: I312bc721736e2edf385ece5141133ffa6bd20a72 Signed-off-by: Terin Stock <terinjokes@gmail.com>
-rw-r--r--.gitattributes2
-rw-r--r--.gitignore3
-rw-r--r--Gopkg.lock104
-rw-r--r--Gopkg.toml29
-rw-r--r--LICENSE.md19
-rw-r--r--cmd/playsmf/main.go114
-rw-r--r--cmd/playsmf/main_test.go48
-rw-r--r--cmd/playsmf/testdata/ceottk-output.golden16
-rw-r--r--cmd/playsmf/testdata/ceottk-serial.golden.binbin0 -> 32 bytes
-rw-r--r--cmd/playsmf/testdata/ceottk.midbin0 -> 209 bytes
-rw-r--r--pkg/ttymididrv/driver.go73
-rw-r--r--pkg/ttymididrv/out.go79
12 files changed, 487 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..01e9b9f
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Binary files, diff using hexdump
+*.bin binary diff=hex
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1fe50a2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/vendor/
+third/midifile/*.a
+third/midifile/*.o
diff --git a/Gopkg.lock b/Gopkg.lock
new file mode 100644
index 0000000..6c7468d
--- /dev/null
+++ b/Gopkg.lock
@@ -0,0 +1,104 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+ digest = "1:2e3c336fc7fde5c984d2841455a658a6d626450b1754a854b3b32e7a8f49a07a"
+ name = "github.com/google/go-cmp"
+ packages = [
+ "cmp",
+ "cmp/internal/diff",
+ "cmp/internal/function",
+ "cmp/internal/value",
+ ]
+ pruneopts = "UT"
+ revision = "3af367b6b30c263d47e8895973edcca9a49cf029"
+ version = "v0.2.0"
+
+[[projects]]
+ digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
+ name = "github.com/pkg/errors"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
+ version = "v0.8.0"
+
+[[projects]]
+ branch = "master"
+ digest = "1:4f43b3c1b7e44980a5f3c593f8bf0e18844dc44f451a071c93e77e28cf990db6"
+ name = "github.com/pkg/term"
+ packages = ["termios"]
+ pruneopts = "UT"
+ revision = "cda20d4ac917ad418d86e151eff439648b06185b"
+
+[[projects]]
+ branch = "master"
+ digest = "1:b9b666a56e920eaa59ffea0d25a9b848d7073be13689c4a29d04e4a0f548a031"
+ name = "github.com/tarm/serial"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "eaafced92e9619f03c72527efeab21e326f3bc36"
+
+[[projects]]
+ digest = "1:60550785588e9d2a3afcb619b2cb92e802183bc0e53ec2aedaeefa2ebf6700d0"
+ name = "gitlab.com/gomidi/midi"
+ packages = [
+ ".",
+ "cc",
+ "internal/midilib",
+ "internal/runningstatus",
+ "internal/vlq",
+ "mid",
+ "midimessage",
+ "midimessage/channel",
+ "midimessage/meta",
+ "midimessage/meta/meter",
+ "midimessage/realtime",
+ "midimessage/syscommon",
+ "midimessage/sysex",
+ "midireader",
+ "midiwriter",
+ "smf",
+ "smf/smfreader",
+ "smf/smfwriter",
+ ]
+ pruneopts = "UT"
+ revision = "2499eaf73227fafd8e1ca0a625c569066835d695"
+ version = "v1.7.9"
+
+[[projects]]
+ branch = "master"
+ digest = "1:d773e525476aefa22ea944a5425a9bfb99819b2e67eeb9b1966454fd57522bbf"
+ name = "golang.org/x/sys"
+ packages = ["unix"]
+ pruneopts = "UT"
+ revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4"
+
+[[projects]]
+ digest = "1:23fe6c6a60e25700c53335117c4176780d207c7799958ad051d0a8b82b00efb6"
+ name = "gotest.tools"
+ packages = [
+ "assert",
+ "assert/cmp",
+ "golden",
+ "internal/difflib",
+ "internal/format",
+ "internal/source",
+ ]
+ pruneopts = "UT"
+ revision = "b6e20af1ed078cd01a6413b734051a292450b4cb"
+ version = "v2.1.0"
+
+[solve-meta]
+ analyzer-name = "dep"
+ analyzer-version = 1
+ input-imports = [
+ "github.com/pkg/term/termios",
+ "github.com/tarm/serial",
+ "gitlab.com/gomidi/midi",
+ "gitlab.com/gomidi/midi/mid",
+ "gitlab.com/gomidi/midi/midimessage",
+ "gitlab.com/gomidi/midi/smf",
+ "gotest.tools/golden",
+ ]
+ solver-name = "gps-cdcl"
+ solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
new file mode 100644
index 0000000..2765dc9
--- /dev/null
+++ b/Gopkg.toml
@@ -0,0 +1,29 @@
+# Gopkg.toml example
+#
+# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
+# for detailed Gopkg.toml documentation.
+#
+# required = ["github.com/user/thing/cmd/thing"]
+# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
+#
+# [[constraint]]
+# name = "github.com/user/project"
+# version = "1.0.0"
+#
+# [[constraint]]
+# name = "github.com/user/project2"
+# branch = "dev"
+# source = "github.com/myfork/project2"
+#
+# [[override]]
+# name = "github.com/x/y"
+# version = "2.4.0"
+#
+# [prune]
+# non-go = false
+# go-tests = true
+# unused-packages = true
+
+[prune]
+ go-tests = true
+ unused-packages = true
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..27bad8d
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright 2018 Terin Stock
+
+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/cmd/playsmf/main.go b/cmd/playsmf/main.go
new file mode 100644
index 0000000..7b55ff3
--- /dev/null
+++ b/cmd/playsmf/main.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "container/list"
+ "flag"
+ "fmt"
+ "os"
+ "time"
+
+ "gitlab.com/gomidi/midi"
+ "gitlab.com/gomidi/midi/mid"
+ "gitlab.com/gomidi/midi/midimessage"
+ "gitlab.com/gomidi/midi/smf"
+ "go.terinstock.com/midifi/pkg/ttymididrv"
+)
+
+var (
+ drv mid.Driver
+ midiSerial string
+)
+
+func init() {
+ flag.StringVar(&midiSerial, "com", "", "The serial port of the MIDI synth")
+}
+
+func main() {
+ flag.Parse()
+ drv = ttymididrv.New(midiSerial, 38400)
+ defer drv.Close()
+
+ outs, err := drv.Outs()
+ if err != nil {
+ fmt.Printf("err: %v\n", err)
+ os.Exit(-1)
+ }
+ out := outs[0]
+
+ err = out.Open()
+ if err != nil {
+ fmt.Printf("err: %v\n", err)
+ os.Exit(-1)
+ }
+ defer out.Close()
+
+ if flag.NArg() == 0 {
+ flag.Usage()
+ os.Exit(-1)
+ }
+
+ f, err := os.Open(flag.Arg(0))
+ if err != nil {
+ fmt.Println("unable to load SMF file")
+ os.Exit(-2)
+ }
+ defer f.Close()
+
+ fmt.Printf("file: %s\n", flag.Arg(0))
+
+ mf := mid.NewReader(mid.NoLogger())
+ mf.SMFHeader = func(header smf.Header) {
+ fmt.Printf("file format: %s\n", header.Format.String())
+ fmt.Printf("number of tracks: %d\n", header.NumTracks)
+ fmt.Printf("time format: %s\n", header.TimeFormat.String())
+ fmt.Println("===========")
+ }
+ events := &list.List{}
+ mf.Msg.Each = func(pos *mid.Position, msg midi.Message) {
+ addEvent(events, Event{track: pos.Track, abs: pos.AbsoluteTicks, msg: msg})
+ }
+ mf.ReadSMF(f)
+
+ w := mid.WriteTo(out)
+
+ var lastDur time.Duration
+ for event := events.Front(); event != nil; event = event.Next() {
+ dur := *mf.TimeAt(event.Value.(Event).abs)
+ if lastDur != dur {
+ <-time.After(dur - lastDur)
+ lastDur = dur
+ }
+
+ msg := event.Value.(Event).msg
+ if midimessage.IsLive(msg) {
+ fmt.Printf("%s\n", msg.String())
+ w.Write(msg)
+ }
+ }
+}
+
+type Event struct {
+ track int16
+ abs uint64
+ msg midi.Message
+}
+
+func addEvent(l *list.List, evt Event) {
+ if l.Front() == nil {
+ l.PushBack(evt)
+ return
+ }
+
+ for cur := l.Front(); cur != nil; cur = cur.Next() {
+ peek := cur.Next()
+ if peek == nil {
+ l.InsertAfter(evt, cur)
+ return
+ }
+
+ if peek.Value.(Event).abs >= evt.abs {
+ l.InsertAfter(evt, cur)
+ return
+ }
+ }
+}
diff --git a/cmd/playsmf/main_test.go b/cmd/playsmf/main_test.go
new file mode 100644
index 0000000..68ee28f
--- /dev/null
+++ b/cmd/playsmf/main_test.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "testing"
+
+ "github.com/pkg/term/termios"
+ "gotest.tools/golden"
+)
+
+func TestMain(m *testing.M) {
+ switch os.Getenv("GOTESTMODE") {
+ case "main":
+ // run the normal main function
+ main()
+ default:
+ os.Exit(m.Run())
+ }
+}
+
+func TestCLI(t *testing.T) {
+ pty, tty, err := termios.Pty()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer pty.Close()
+
+ cmd := exec.Command(os.Args[0], "-com", tty.Name(), "./testdata/ceottk.mid")
+ cmd.Env = append(os.Environ(), "GOTESTMODE=main")
+
+ output, err := cmd.Output()
+ if err != nil {
+ fmt.Printf("output: %s\n", string(output))
+ t.Fatal(err)
+ }
+
+ tty.Close()
+ b, err := ioutil.ReadAll(pty)
+ if err != nil && err.Error() != "read ptm: input/output error" {
+ t.Errorf("error reading pty: %+v", err)
+ }
+
+ golden.AssertBytes(t, b, "ceottk-serial.golden.bin")
+ golden.Assert(t, string(output), "ceottk-output.golden")
+}
diff --git a/cmd/playsmf/testdata/ceottk-output.golden b/cmd/playsmf/testdata/ceottk-output.golden
new file mode 100644
index 0000000..4cac21b
--- /dev/null
+++ b/cmd/playsmf/testdata/ceottk-output.golden
@@ -0,0 +1,16 @@
+file: ./testdata/ceottk.mid
+file format: SMF1 (multitrack)
+number of tracks: 2
+time format: 480 MetricTicks
+===========
+channel.NoteOn channel 1 key 79 velocity 81
+channel.ProgramChange channel 1 program 19
+channel.NoteOn channel 1 key 81 velocity 81
+channel.NoteOff channel 1 key 79
+channel.NoteOn channel 1 key 77 velocity 81
+channel.NoteOff channel 1 key 81
+channel.NoteOn channel 1 key 65 velocity 81
+channel.NoteOff channel 1 key 77
+channel.NoteOn channel 1 key 72 velocity 81
+channel.NoteOff channel 1 key 65
+channel.NoteOff channel 1 key 72
diff --git a/cmd/playsmf/testdata/ceottk-serial.golden.bin b/cmd/playsmf/testdata/ceottk-serial.golden.bin
new file mode 100644
index 0000000..a505536
--- /dev/null
+++ b/cmd/playsmf/testdata/ceottk-serial.golden.bin
Binary files differ
diff --git a/cmd/playsmf/testdata/ceottk.mid b/cmd/playsmf/testdata/ceottk.mid
new file mode 100644
index 0000000..4d0877e
--- /dev/null
+++ b/cmd/playsmf/testdata/ceottk.mid
Binary files differ
diff --git a/pkg/ttymididrv/driver.go b/pkg/ttymididrv/driver.go
new file mode 100644
index 0000000..cc7b1fd
--- /dev/null
+++ b/pkg/ttymididrv/driver.go
@@ -0,0 +1,73 @@
+package ttymididrv
+
+import (
+ "sync"
+
+ "gitlab.com/gomidi/midi/mid"
+)
+
+type driver struct {
+ mu sync.RWMutex
+ outs []mid.Out
+ opened []mid.Port
+ closed bool
+}
+
+func New(name string, baud int) mid.Driver {
+ d := &driver{}
+ d.outs = []mid.Out{
+ &out{
+ name: name,
+ baud: baud,
+ number: 0,
+ driver: d,
+ },
+ }
+
+ return d
+}
+
+func (d *driver) String() string {
+ return "ttymididrv"
+}
+
+func (d *driver) Close() (err error) {
+ d.mu.RLock()
+ if d.closed {
+ d.mu.RUnlock()
+ return mid.ErrClosed
+ }
+ d.mu.RUnlock()
+
+ d.mu.Lock()
+ d.closed = true
+ d.mu.Unlock()
+
+ for _, p := range d.opened {
+ err = p.Close()
+ }
+
+ return
+}
+
+func (d *driver) Ins() ([]mid.In, error) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+
+ if d.closed {
+ return nil, mid.ErrClosed
+ }
+
+ return nil, nil
+}
+
+func (d *driver) Outs() ([]mid.Out, error) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+
+ if d.closed {
+ return nil, mid.ErrClosed
+ }
+
+ return d.outs, nil
+}
diff --git a/pkg/ttymididrv/out.go b/pkg/ttymididrv/out.go
new file mode 100644
index 0000000..3e16cff
--- /dev/null
+++ b/pkg/ttymididrv/out.go
@@ -0,0 +1,79 @@
+package ttymididrv
+
+import (
+ "sync"
+
+ "github.com/tarm/serial"
+)
+
+type out struct {
+ mu sync.RWMutex
+ driver *driver
+ port *serial.Port
+ name string
+ baud int
+ closed bool
+ number int
+}
+
+func (o *out) Open() (err error) {
+ o.mu.RLock()
+ if o.closed || o.port != nil {
+ o.mu.RUnlock()
+ return nil
+ }
+ o.mu.RUnlock()
+
+ o.mu.Lock()
+ defer o.mu.Unlock()
+
+ c := &serial.Config{Name: o.name, Baud: o.baud}
+ o.port, err = serial.OpenPort(c)
+ if err != nil {
+ return err
+ }
+
+ o.driver.mu.Lock()
+ o.driver.opened = append(o.driver.opened, o)
+ o.driver.mu.Unlock()
+
+ return nil
+}
+
+func (o *out) Close() error {
+ o.mu.RLock()
+ if o.closed || o.port == nil {
+ o.mu.RUnlock()
+ return nil
+ }
+ o.mu.RUnlock()
+
+ o.mu.Lock()
+ o.closed = true
+ o.mu.Unlock()
+
+ return o.port.Close()
+}
+
+func (o *out) IsOpen() bool {
+ o.mu.RLock()
+ defer o.mu.RUnlock()
+ return !o.closed && o.port != nil
+}
+
+func (o *out) Number() int {
+ return o.number
+}
+
+func (o *out) String() string {
+ return o.name
+}
+
+func (o *out) Underlying() interface{} {
+ return o.port
+}
+
+func (o *out) Send(b []byte) error {
+ _, err := o.port.Write(b)
+ return err
+}