diff options
author | Terin Stock <terinjokes@gmail.com> | 2018-07-29 19:37:27 +0000 |
---|---|---|
committer | Terin Stock <terinjokes@gmail.com> | 2019-01-27 17:55:12 -0800 |
commit | ca933d746ba16ef326d9b4ba85d51940ab9e8add (patch) | |
tree | 203e060468f27bb05095aaa1cb755d7c961bd530 | |
parent | 7af81de12cde42985291950b0e5e2ab2230acd09 (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-- | .gitattributes | 2 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Gopkg.lock | 104 | ||||
-rw-r--r-- | Gopkg.toml | 29 | ||||
-rw-r--r-- | LICENSE.md | 19 | ||||
-rw-r--r-- | cmd/playsmf/main.go | 114 | ||||
-rw-r--r-- | cmd/playsmf/main_test.go | 48 | ||||
-rw-r--r-- | cmd/playsmf/testdata/ceottk-output.golden | 16 | ||||
-rw-r--r-- | cmd/playsmf/testdata/ceottk-serial.golden.bin | bin | 0 -> 32 bytes | |||
-rw-r--r-- | cmd/playsmf/testdata/ceottk.mid | bin | 0 -> 209 bytes | |||
-rw-r--r-- | pkg/ttymididrv/driver.go | 73 | ||||
-rw-r--r-- | pkg/ttymididrv/out.go | 79 |
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 +} |