summary refs log tree commit diff
diff options
context:
space:
mode:
authorTerin Stock <terinjokes@gmail.com>2018-07-29 19:37:27 +0000
committerTerin Stock <terinjokes@gmail.com>2019-01-27 17:55:12 -0800
commitca933d746ba16ef326d9b4ba85d51940ab9e8add (patch)
tree203e060468f27bb05095aaa1cb755d7c961bd530
parent7af81de12cde42985291950b0e5e2ab2230acd09 (diff)
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 differdiff --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 differdiff --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
+}