midi
Modular library for reading and writing of MIDI messages and MIDI files with Go.
Note: If you are reading this on Github, please note that the repo has moved to Gitlab (gitlab.com/gomidi/midi) and this is only a mirror.
- Go version: >= 1.14
- OS/architectures: everywhere Go runs (tested on Linux and Windows).
Installation
go get gitlab.com/gomidi/midi@latest
Features
This package provides a unified way to read and write "over the wire" MIDI data and MIDI files (SMF).
Drivers
For "cable" communication you need a Driver
to connect with the MIDI system of your OS.
Currently the following drivers available (all multi-platform):
- package
gitlab.com/gomidi/rtmididrv
based on rtmidi (requires CGO) - package
gitlab.com/gomidi/portmididrv
based on portmidi (requires CGO) - package
gitlab.com/gomidi/webmididrv
based on the Web MIDI standard (produces webassembly) - package
gitlab.com/gomidi/midicatdrv
based on the midicat binaries via piping (stdin / stdout) (no CGO needed) - package
gitlab.com/gomidi/midi/testdrv
for testing (no CGO needed)
Projects using this library
Porcelain package
For easy access, the following packages are recommended:
- reading:
gitlab.com/gomidi/midi/reader
- writing:
gitlab.com/gomidi/midi/writer
The other packages are more low level and allow you to write your own implementations of the midi.Reader
, midi.Writer
and midi.Driver
interfaces to wrap the given SMF and live readers/writers/drivers for your own application.
Example with MIDI cables
package main
import (
"fmt"
"time"
"gitlab.com/gomidi/midi"
"gitlab.com/gomidi/midi/reader"
driver "gitlab.com/gomidi/midi/testdrv"
"gitlab.com/gomidi/midi/writer"
)
func main() {
drv := driver.New("fake cables: messages written to output port 0 are received on input port 0")
defer drv.Close()
ins, err := drv.Ins()
must(err)
outs, err := drv.Outs()
must(err)
in, out := ins[0], outs[0]
must(in.Open())
must(out.Open())
defer in.Close()
defer out.Close()
wr := writer.New(out)
rd := reader.New(
reader.NoLogger(),
reader.Each(func(pos *reader.Position, msg midi.Message) {
fmt.Printf("got %s\n", msg)
}),
)
err = rd.ListenTo(in)
must(err)
err = writer.NoteOn(wr, 60, 100)
must(err)
time.Sleep(1)
err = writer.NoteOff(wr, 60)
must(err)
}
func must(err error) {
if err != nil {
panic(err.Error())
}
}
Example with MIDI file (SMF)
package main
import (
"fmt"
"os"
"path/filepath"
"gitlab.com/gomidi/midi/reader"
"gitlab.com/gomidi/midi/writer"
)
type printer struct{}
func (pr printer) noteOn(p *reader.Position, channel, key, vel uint8) {
fmt.Printf("Track: %v Pos: %v NoteOn (ch %v: key %v vel: %v)\n", p.Track, p.AbsoluteTicks, channel, key, vel)
}
func (pr printer) noteOff(p *reader.Position, channel, key, vel uint8) {
fmt.Printf("Track: %v Pos: %v NoteOff (ch %v: key %v)\n", p.Track, p.AbsoluteTicks, channel, key)
}
func main() {
dir := os.TempDir()
f := filepath.Join(dir, "smf-test.mid")
defer os.Remove(f)
var p printer
err := writer.WriteSMF(f, 2, func(wr *writer.SMF) error {
wr.SetChannel(11)
writer.NoteOn(wr, 120, 50)
wr.SetDelta(120)
writer.NoteOff(wr, 120)
wr.SetDelta(240)
writer.NoteOn(wr, 125, 50)
wr.SetDelta(20)
writer.NoteOff(wr, 125)
writer.EndOfTrack(wr)
wr.SetChannel(2)
writer.NoteOn(wr, 120, 50)
wr.SetDelta(60)
writer.NoteOff(wr, 120)
writer.EndOfTrack(wr)
return nil
})
if err != nil {
fmt.Printf("could not write SMF file %v\n", f)
return
}
rd := reader.New(reader.NoLogger(),
reader.NoteOn(p.noteOn),
reader.NoteOff(p.noteOff),
)
err = reader.ReadSMFFile(rd, f)
if err != nil {
fmt.Printf("could not read SMF file %v\n", f)
}
}
Low level packages
Example with low level packages
package main
import (
"bytes"
"fmt"
. "gitlab.com/gomidi/midi/midimessage/channel"
"gitlab.com/gomidi/midi/midimessage/realtime"
"gitlab.com/gomidi/midi/midireader"
"gitlab.com/gomidi/midi/midiwriter"
"io"
"gitlab.com/gomidi/midi"
)
func main() {
var bf bytes.Buffer
wr := midiwriter.New(&bf)
wr.Write(Channel2.Pitchbend(5000))
wr.Write(Channel2.NoteOn(65, 90))
wr.Write(realtime.Reset)
wr.Write(Channel2.NoteOff(65))
rthandler := func(m realtime.Message) {
fmt.Printf("Realtime: %s\n", m)
}
rd := midireader.New(bytes.NewReader(bf.Bytes()), rthandler)
var m midi.Message
var err error
for {
m, err = rd.Read()
if err != nil {
break
}
fmt.Println(m)
switch v := m.(type) {
case NoteOn:
fmt.Printf("NoteOn at channel %v: key: %v velocity: %v\n", v.Channel(), v.Key(), v.Velocity())
case NoteOff:
fmt.Printf("NoteOff at channel %v: key: %v\n", v.Channel(), v.Key())
}
}
if err != io.EOF {
panic("error: " + err.Error())
}
}
Modularity
Apart from the porcelain packages there are small subpackages, so that you only need to import
what you really need.
This keeps packages and dependencies small, better testable and should result in a smaller memory footprint which should help smaller devices.
For reading and writing of cable and SMF MIDI data io.Readers
are accepted as input and io.Writers
as output. Furthermore there are common interfaces for live and SMF MIDI data handling: midi.Reader
and midi.Writer
. The typed MIDI messages used in each case are the same.
To connect with MIDI libraries expecting and returning plain bytes, use the midiio
subpackage.
Stability / API / Semantic Versioning
This excellent blog post by Peter Bourgon describes
perfectly the problem, I have with Go modules
' take on semantic versioning
.
First of all, the API of this library is large (if you use all of it, which is unlikely) and generally
stable. There may be small incompatibilities from time to time that will only affect a small amount of users and
that will "blow up into your face" when compiling. These are easy to fix with the help of the compiler
and the documentation.
The diamond dependency problem
should be unlikely with this library, since that would mean,
you are using a library that is abstracting over MIDI and uses this library in order to do so.
Well, then you should reconsider the use of that library, since there really is no point in further abstractions.
For other uses however (e.g. integration), the interfaces should be used and they won't change.
As a last resort, you could fork that other library, add a replace statement to your go.mod
and fix the issue, followed by a pull request. If that libary requiring the old code is not maintained anymore,
you would need to fork anyway in the future. If it is maintained, your pull request should get accepted.
We are not going to bump the major version with every tiny incompatible change of this code base.
The costs are too high and the benefits too low. Instead, any minor
version upgrade reflects incompatible changes and
the patch
versions contain compatible changes (i.e. also compatible additions). A large rewrite will however end up in a major version bump.
License
MIT (see LICENSE file)
Credits
Inspiration and low level code for MIDI reading (see internal midilib package) came from the http://github.com/afandian/go-midi package of Joe Wass which also helped as a starting point for the reading of SMF files.
Alternatives
Matt Aimonetti is also working on MIDI inside https://github.com/mattetti/audio but I didn't try it.