├── Makefile ├── README.md ├── cmd └── midi │ └── main.go ├── go.mod ├── go.sum ├── keystation_test.go ├── list_test.go ├── mem.c ├── mem.h ├── midi.go ├── midi_darwin.c ├── midi_darwin.go ├── midi_darwin.h ├── midi_linux.c ├── midi_linux.go ├── midi_linux.h └── midi_test.go /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @go test 3 | 4 | .PHONY: test 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # midi 2 | 3 | Dead simple MIDI package for Go. 4 | Currently only supports Linux and Mac. 5 | 6 | ## Install 7 | 8 | ``` 9 | go get github.com/scgolang/midi 10 | ``` 11 | 12 | If you're on Linux, you'll have to install the ALSA development files. 13 | 14 | The package is probably named either `libasound2-dev` or `alsa-devel`. 15 | -------------------------------------------------------------------------------- /cmd/midi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/scgolang/midi" 9 | ) 10 | 11 | // Config represents the application's configuration. 12 | type Config struct { 13 | List bool 14 | } 15 | 16 | func main() { 17 | var config Config 18 | 19 | flag.BoolVar(&config.List, "l", false, "list midi devices") 20 | flag.Parse() 21 | 22 | exitCode := 0 23 | 24 | if config.List { 25 | exitCode = list() 26 | } 27 | os.Exit(exitCode) 28 | } 29 | 30 | func list() int { 31 | devices, err := midi.Devices() 32 | if err != nil { 33 | fmt.Fprintln(os.Stderr, err.Error()) 34 | return 1 35 | } 36 | fmt.Printf("found %d devices\n", len(devices)) 37 | 38 | for _, device := range devices { 39 | fmt.Printf("%s %s %s\n", device.ID, device.Name, device.Type) 40 | } 41 | return 0 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scgolang/midi 2 | 3 | go 1.19 4 | 5 | require github.com/pkg/errors v0.9.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 2 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /keystation_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestKeystation(t *testing.T) { 10 | devices, err := Devices() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | var keystation *Device 15 | for _, d := range devices { 16 | if strings.Contains(strings.ToLower(d.Name), "keystation") { 17 | keystation = d 18 | break 19 | } 20 | } 21 | if keystation == nil { 22 | t.Log("no keystation detected") 23 | t.SkipNow() 24 | } 25 | fmt.Println("keystation detected") 26 | 27 | if err := keystation.Open(); err != nil { 28 | t.Fatal(err) 29 | } 30 | packets, err := keystation.Packets() 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | i := 0 35 | for pkt := range packets { 36 | if i == 4 { 37 | break 38 | } 39 | fmt.Printf("%#v\n", pkt) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDevices(t *testing.T) { 8 | if _, err := Devices(); err != nil { 9 | t.Fatal(err) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mem.c: -------------------------------------------------------------------------------- 1 | // +build cgo 2 | #include 3 | #include 4 | #include 5 | 6 | #include "mem.h" 7 | 8 | void *midi_mem_alloc(long nbytes, const char *file, int line) { 9 | void *p; 10 | assert(nbytes > 0); 11 | p = malloc(nbytes); 12 | // crash if oom 13 | assert(p != NULL); 14 | return p; 15 | } 16 | 17 | void *midi_mem_calloc(int count, long nbytes, const char *file, int line) { 18 | void *p; 19 | assert(count > 0); 20 | assert(nbytes > 0); 21 | p = calloc(count, nbytes); 22 | assert(p != NULL); 23 | return p; 24 | } 25 | 26 | void midi_mem_free(void * ptr, const char *file, int line) { 27 | if (ptr) 28 | free(ptr); 29 | } 30 | 31 | void *midi_mem_resize(void *ptr, long nbytes, const char *file, int line) { 32 | assert(ptr); 33 | assert(nbytes > 0); 34 | ptr = realloc(ptr, nbytes); 35 | assert(ptr != NULL); 36 | return ptr; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /mem.h: -------------------------------------------------------------------------------- 1 | #ifndef MEM_H_INCLUDED 2 | #define MEM_H_INCLUDED 3 | 4 | extern void *midi_mem_alloc(long nbytes, const char * file, int line); 5 | extern void *midi_mem_calloc(int count, long nbytes, const char * file, int line); 6 | extern void midi_mem_free(void *ptr, const char *file, int line); 7 | extern void *midi_mem_resize(void *ptr, long nbytes, const char *file, int line); 8 | 9 | #define ALLOC(nbytes) midi_mem_alloc((nbytes), __FILE__, __LINE__) 10 | #define CALLOC(count, nbytes) midi_mem_calloc((count), (nbytes), __FILE__, __LINE__) 11 | 12 | #define NEW(p) ((p) = ALLOC((long) sizeof *(p))) 13 | #define NEW0(p) ((p) = CALLOC(1, (long) sizeof *(p))) 14 | #define FREE(p) ((void) (midi_mem_free((p), __FILE__, __LINE__), (p) = 0)) 15 | #define RESIZE(p, nbytes) ((p) = midi_mem_resize((p), (nbytes), __FILE__, __LINE__)) 16 | 17 | #endif // MEM_H_INCLUDED 18 | -------------------------------------------------------------------------------- /midi.go: -------------------------------------------------------------------------------- 1 | // Package midi is a package for talking to midi devices in Go. 2 | package midi 3 | 4 | // Packet is a MIDI packet. 5 | type Packet struct { 6 | Data [3]byte 7 | Err error 8 | } 9 | 10 | // DeviceType is a flag that says if a device is an input, an output, or duplex. 11 | type DeviceType int 12 | 13 | // Device types. 14 | const ( 15 | DeviceInput DeviceType = iota 16 | DeviceOutput 17 | DeviceDuplex 18 | ) 19 | 20 | func (t DeviceType) String() string { 21 | switch t { 22 | case DeviceInput: 23 | return "INPUT" 24 | case DeviceOutput: 25 | return "OUTPUT" 26 | case DeviceDuplex: 27 | return "DUPLEX" 28 | default: 29 | panic("unrecognized device type") 30 | } 31 | } 32 | 33 | // Note represents a MIDI note. 34 | type Note struct { 35 | Number int 36 | Velocity int 37 | } 38 | 39 | // CC represents a MIDI control change message. 40 | type CC struct { 41 | Number int 42 | Value int 43 | } 44 | 45 | const ( 46 | MessageTypeUnknown = iota 47 | MessageTypeCC 48 | MessageTypeNoteOff 49 | MessageTypeNoteOn 50 | MessageTypePolyKeyPressure 51 | ) 52 | 53 | // GetMessageType returns the message type for the provided packet. 54 | func GetMessageType(p Packet) int { 55 | switch p.Data[0] & 0xF0 { 56 | case 0x80: 57 | return MessageTypeNoteOff 58 | case 0x90: 59 | return MessageTypeNoteOn 60 | default: 61 | return MessageTypeUnknown 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /midi_darwin.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "mem.h" 11 | #include "midi_darwin.h" 12 | 13 | extern void SendPacket(Midi midi, const MIDIPacket *pkt); 14 | 15 | // Midi represents a MIDI connection that uses the ALSA RawMidi API. 16 | struct Midi { 17 | MIDIClientRef client; 18 | MIDIEndpointRef input; 19 | MIDIEndpointRef output; 20 | MIDIPortRef inputPort; 21 | MIDIPortRef outputPort; 22 | }; 23 | 24 | // Midi_open opens a MIDI connection to the specified device. 25 | // If there is an error it returns NULL. 26 | Midi_open_result Midi_open(MIDIEndpointRef input, MIDIEndpointRef output) { 27 | Midi midi; 28 | OSStatus rc; 29 | 30 | NEW(midi); 31 | 32 | midi->input = input; 33 | midi->output = output; 34 | 35 | rc = MIDIClientCreate(CFSTR("scgolang"), NULL, NULL, &midi->client); 36 | if (rc != 0) { 37 | return (Midi_open_result) { .midi = NULL, .error = rc }; 38 | } 39 | rc = MIDIInputPortCreate(midi->client, CFSTR("scgolang input"), Midi_read_proc, NULL, &midi->inputPort); 40 | if (rc != 0) { 41 | return (Midi_open_result) { .midi = NULL, .error = rc }; 42 | } 43 | rc = MIDIOutputPortCreate(midi->client, CFSTR("scgolang output"), &midi->outputPort); 44 | if (rc != 0) { 45 | return (Midi_open_result) { .midi = NULL, .error = rc }; 46 | } 47 | rc = MIDIPortConnectSource(midi->inputPort, input, midi); 48 | if (rc != 0) { 49 | return (Midi_open_result) { .midi = NULL, .error = rc }; 50 | } 51 | return (Midi_open_result) { .midi = midi, .error = 0 }; 52 | } 53 | 54 | // Midi_read_proc is the callback that gets invoked when MIDI data comes int. 55 | void Midi_read_proc(const MIDIPacketList *pkts, void *readProcRefCon, void *srcConnRefCon) { 56 | const MIDIPacket *pkt = &pkts->packet[0]; 57 | 58 | Midi midi = (Midi) srcConnRefCon; 59 | 60 | SendPacket(midi, pkt); 61 | 62 | for (int i = 1; i < pkts->numPackets; i++) { 63 | pkt = MIDIPacketNext(pkt); 64 | SendPacket(midi, pkt); 65 | } 66 | } 67 | 68 | // Midi_write writes bytes to the provided MIDI connection. 69 | Midi_write_result Midi_write(Midi midi, const char *buffer, size_t buffer_size) { 70 | assert(midi); 71 | 72 | MIDIPacketList pkts; 73 | MIDIPacket *cur = MIDIPacketListInit(&pkts); 74 | MIDITimeStamp now = mach_absolute_time(); 75 | size_t numPackets = (buffer_size / 256) + 1; 76 | ByteCount listSize = numPackets * 256; 77 | 78 | for (size_t i = 0; i < numPackets; i++) { 79 | Byte data[3]; 80 | for (int j = 0; j < 3; j++) { 81 | data[j] = buffer[i+j]; 82 | } 83 | cur = MIDIPacketListAdd(&pkts, listSize, cur, now, 3, data); 84 | if (cur == NULL) { 85 | return (Midi_write_result) { .n = 0, .error = -10900 }; 86 | } 87 | } 88 | OSStatus rc = MIDISend(midi->outputPort, midi->output, &pkts); 89 | if (rc != 0) { 90 | return (Midi_write_result) { .n = 0, .error = rc }; 91 | } 92 | return (Midi_write_result) { .n = listSize, .error = 0 }; 93 | } 94 | 95 | // Midi_close closes a MIDI connection. 96 | int Midi_close(Midi midi) { 97 | assert(midi); 98 | 99 | OSStatus rc1, rc2, rc3, rc4, rc5; 100 | 101 | rc1 = MIDIPortDispose(midi->inputPort); 102 | rc2 = MIDIPortDispose(midi->outputPort); 103 | rc3 = MIDIClientDispose(midi->client); 104 | rc4 = MIDIEndpointDispose(midi->input); 105 | rc5 = MIDIEndpointDispose(midi->output); 106 | 107 | FREE(midi); 108 | 109 | if (rc1 != 0) return rc1; 110 | else if (rc2 != 0) return rc2; 111 | else if (rc3 != 0) return rc3; 112 | else if (rc4 != 0) return rc4; 113 | else if (rc5 != 0) return rc5; 114 | else return 0; 115 | } 116 | 117 | // CFStringToUTF8 converts a CoreFoundation string to a UTF-encoded C string. 118 | // Callers are responsible for free'ing the returned string. 119 | char *CFStringToUTF8(CFStringRef aString) { 120 | if (aString == NULL) { 121 | return NULL; 122 | } 123 | CFIndex length = CFStringGetLength(aString); 124 | CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; 125 | char *buffer = (char *)malloc(maxSize); 126 | if (CFStringGetCString(aString, buffer, maxSize, kCFStringEncodingUTF8)) { 127 | return buffer; 128 | } 129 | free(buffer); // If we failed 130 | return NULL; 131 | } 132 | -------------------------------------------------------------------------------- /midi_darwin.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include "midi_darwin.h" 7 | // #cgo darwin LDFLAGS: -framework CoreFoundation -framework CoreMIDI 8 | import "C" 9 | 10 | import ( 11 | "sync" 12 | "unsafe" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // Common errors. 18 | var ( 19 | ErrNotOpen = errors.New("Did you remember to open the device?") 20 | ) 21 | 22 | var ( 23 | packetChans = map[*Device]chan []Packet{} 24 | packetChansMutex sync.RWMutex 25 | ) 26 | 27 | // Device provides an interface for MIDI devices. 28 | type Device struct { 29 | ID string 30 | Name string 31 | Type DeviceType 32 | 33 | // QueueSize controls the buffer size of the read channel. Use 0 for blocking reads. 34 | QueueSize int 35 | 36 | conn C.Midi 37 | input C.MIDIEndpointRef 38 | output C.MIDIEndpointRef 39 | } 40 | 41 | // Open opens a MIDI device. 42 | // queueSize is the number of packets to buffer in the channel associated with the device. 43 | func (d *Device) Open() error { 44 | result := C.Midi_open(d.input, d.output) 45 | if result.error != 0 { 46 | return coreMidiError(result.error) 47 | } 48 | d.conn = result.midi 49 | packetChansMutex.Lock() 50 | packetChans[d] = make(chan []Packet, d.QueueSize) 51 | packetChansMutex.Unlock() 52 | return nil 53 | } 54 | 55 | // Close closes the connection to the MIDI device. 56 | func (d *Device) Close() error { 57 | return coreMidiError(C.OSStatus(C.Midi_close(d.conn))) 58 | } 59 | 60 | // Packets emits MIDI packets. 61 | // If the device has not been opened it will return ErrNotOpen. 62 | func (d *Device) Packets() (<-chan []Packet, error) { 63 | packetChansMutex.RLock() 64 | for device, packetChan := range packetChans { 65 | if d.conn == device.conn { 66 | packetChansMutex.RUnlock() 67 | return packetChan, nil 68 | } 69 | } 70 | packetChansMutex.RUnlock() 71 | return nil, ErrNotOpen 72 | } 73 | 74 | // Write writes data to a MIDI device. 75 | func (d *Device) Write(buf []byte) (int, error) { 76 | result := C.Midi_write(d.conn, C.CString(string(buf)), C.size_t(len(buf))) 77 | if result.error != 0 { 78 | return 0, coreMidiError(result.error) 79 | } 80 | return len(buf), nil 81 | } 82 | 83 | //export SendPacket 84 | func SendPacket(conn C.Midi, pkt *C.MIDIPacket) { 85 | var ch chan []Packet 86 | 87 | packetChansMutex.RLock() 88 | for device, packetChan := range packetChans { 89 | if device.conn == conn { 90 | ch = packetChan 91 | } 92 | break 93 | } 94 | packetChansMutex.RUnlock() 95 | 96 | if ch == nil { 97 | return 98 | } 99 | var pkts []Packet 100 | 101 | for i := C.UInt16(0); i < (pkt.length / 3); i++ { 102 | pkts = append(pkts, Packet{ 103 | Data: [3]byte{ 104 | byte(pkt.data[(i*3)+0]), 105 | byte(pkt.data[(i*3)+1]), 106 | byte(pkt.data[(i*3)+2]), 107 | }, 108 | }) 109 | } 110 | ch <- pkts 111 | } 112 | 113 | // coreMidiError maps a CoreMIDI error code to a Go error. 114 | func coreMidiError(code C.OSStatus) error { 115 | switch code { 116 | case 0: 117 | return nil 118 | case C.kMIDIInvalidClient: 119 | return errors.New("an invalid MIDIClientRef was passed") 120 | case C.kMIDIInvalidPort: 121 | return errors.New("an invalid MIDIPortRef was passed") 122 | case C.kMIDIWrongEndpointType: 123 | return errors.New("a source endpoint was passed to a function expecting a destination, or vice versa") 124 | case C.kMIDINoConnection: 125 | return errors.New("attempt to close a non-existant connection") 126 | case C.kMIDIUnknownEndpoint: 127 | return errors.New("an invalid MIDIEndpointRef was passed") 128 | case C.kMIDIUnknownProperty: 129 | return errors.New("attempt to query a property not set on the object") 130 | case C.kMIDIWrongPropertyType: 131 | return errors.New("attempt to set a property with a value not of the correct type") 132 | case C.kMIDINoCurrentSetup: 133 | return errors.New("there is no current MIDI setup object") 134 | case C.kMIDIMessageSendErr: 135 | return errors.New("communication with MIDIServer failed") 136 | case C.kMIDIServerStartErr: 137 | return errors.New("unable to start MIDIServer") 138 | case C.kMIDISetupFormatErr: 139 | return errors.New("unable to read the saved state") 140 | case C.kMIDIWrongThread: 141 | return errors.New("a driver is calling a non-I/O function in the server from a thread other than the server's main thread") 142 | case C.kMIDIObjectNotFound: 143 | return errors.New("the requested object does not exist") 144 | case C.kMIDIIDNotUnique: 145 | return errors.New("attempt to set a non-unique kMIDIPropertyUniqueID on an object") 146 | case C.kMIDINotPermitted: 147 | return errors.New("attempt to perform an operation that is not permitted") 148 | case -10900: 149 | // See Midi_write in midi_darwin.c if you're curious where the number comes from. 150 | // [briansorahan] 151 | // I tried to add a const to midi_darwin.h for this number, 152 | // but it resulted in link errors: 153 | // duplicate symbol _kInsufficientSpaceInPacket in: 154 | // $WORK/github.com/scgolang/midi/_test/_obj_test/_cgo_export.o 155 | // $WORK/github.com/scgolang/midi/_test/_obj_test/midi_darwin.cgo2.o 156 | // duplicate symbol _kInsufficientSpaceInPacket in: 157 | // $WORK/github.com/scgolang/midi/_test/_obj_test/_cgo_export.o 158 | // $WORK/github.com/scgolang/midi/_test/_obj_test/midi_darwin.o 159 | // ld: 2 duplicate symbols for architecture x86_64 160 | return errors.New("insufficient space in packet") 161 | default: 162 | return errors.Errorf("unknown CoreMIDI error: %d", code) 163 | } 164 | } 165 | 166 | // Devices returns a list of devices. 167 | func Devices() ([]*Device, error) { 168 | var ( 169 | maxEndpoints C.ItemCount 170 | devices = []*Device{} 171 | numDestinations C.ItemCount = C.MIDIGetNumberOfDestinations() 172 | numSources C.ItemCount = C.MIDIGetNumberOfSources() 173 | ) 174 | if numDestinations > numSources { 175 | maxEndpoints = numDestinations 176 | } else { 177 | maxEndpoints = numSources 178 | } 179 | for i := C.ItemCount(0); i < maxEndpoints; i++ { 180 | var ( 181 | d *Device 182 | obj C.MIDIObjectRef 183 | ) 184 | if i < numDestinations && i < numSources { 185 | d = &Device{ 186 | Type: DeviceDuplex, 187 | input: C.MIDIGetSource(i), 188 | output: C.MIDIGetDestination(i), 189 | } 190 | obj = C.MIDIObjectRef(d.output) 191 | } else if i < numDestinations { 192 | d = &Device{ 193 | Type: DeviceOutput, 194 | output: C.MIDIGetDestination(i), 195 | } 196 | obj = C.MIDIObjectRef(d.output) 197 | } else { 198 | d = &Device{ 199 | Type: DeviceInput, 200 | input: C.MIDIGetSource(i), 201 | } 202 | obj = C.MIDIObjectRef(d.input) 203 | } 204 | var name C.CFStringRef 205 | if rc := C.MIDIObjectGetStringProperty(obj, C.kMIDIPropertyName, &name); rc != 0 { 206 | return nil, coreMidiError(rc) 207 | } 208 | d.Name = fromCFString(name) 209 | C.CFRelease(C.CFTypeRef(name)) 210 | devices = append(devices, d) 211 | } 212 | return devices, nil 213 | } 214 | 215 | func fromCFString(cfs C.CFStringRef) string { 216 | var ( 217 | cs = C.CFStringToUTF8(cfs) 218 | gs = C.GoString(cs) 219 | ) 220 | C.free(unsafe.Pointer(cs)) 221 | return gs 222 | } 223 | -------------------------------------------------------------------------------- /midi_darwin.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDI_DARWIN_H 2 | #define MIDI_DARWIN_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | // InsufficientSpaceInPacket occurs when you attempt to add a packet 11 | // to a MIDIPacketList which doesn't have enough space to hold the packet. 12 | // Defining this const here causes link errors. 13 | // See the coreMidiError helper in midi_darwin.go [briansorahan] 14 | /* const OSStatus InsufficientSpaceInPacket = -10900; */ 15 | 16 | // Midi represents a connection to a MIDI device. 17 | typedef struct Midi *Midi; 18 | 19 | // Midi_open_result enables us to return both the Midi instance and an error from Midi_open. 20 | typedef struct Midi_open_result { 21 | Midi midi; 22 | OSStatus error; 23 | } Midi_open_result; 24 | 25 | typedef struct Midi_device_endpoints { 26 | MIDIDeviceRef device; 27 | MIDIEndpointRef input; 28 | MIDIEndpointRef output; 29 | OSStatus error; 30 | } Midi_device_endpoints; 31 | 32 | // Midi_open opens a MIDI connection to the specified device. 33 | Midi_open_result Midi_open(MIDIEndpointRef input, MIDIEndpointRef output); 34 | 35 | // Midi_read_proc is the callback that gets invoked when MIDI data comes in. 36 | void Midi_read_proc(const MIDIPacketList *pkts, void *readProcRefCon, void *srcConnRefCon); 37 | 38 | // Midi_write_result enables us to return both a ByteCount and an error from Midi_write. 39 | typedef struct Midi_write_result { 40 | ByteCount n; 41 | OSStatus error; 42 | } Midi_write_result; 43 | 44 | // Midi_write writes bytes to the provided MIDI connection. 45 | Midi_write_result Midi_write(Midi midi, const char *buffer, size_t buffer_size); 46 | 47 | // Midi_close closes a MIDI connection. 48 | int Midi_close(Midi midi); 49 | 50 | // CFStringToUTF8 converts a CFStringRef to a UTF8-encoded C string. 51 | char *CFStringToUTF8(CFStringRef aString); 52 | 53 | #endif // MIDI_DARWIN_H defined 54 | -------------------------------------------------------------------------------- /midi_linux.c: -------------------------------------------------------------------------------- 1 | // +build cgo 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "mem.h" 8 | #include "midi_linux.h" 9 | 10 | // Midi represents a MIDI connection that uses the ALSA RawMidi API. 11 | struct Midi { 12 | snd_rawmidi_t *in; 13 | snd_rawmidi_t *out; 14 | }; 15 | 16 | // Midi_open opens a MIDI connection to the specified device. 17 | // If there is an error it returns NULL. 18 | Midi_open_result Midi_open(const char *name) { 19 | Midi midi; 20 | int rc; 21 | 22 | NEW(midi); 23 | 24 | rc = snd_rawmidi_open(&midi->in, &midi->out, name, SND_RAWMIDI_SYNC); 25 | if (rc != 0) { 26 | return (Midi_open_result) { .midi = NULL, .error = rc }; 27 | } 28 | return (Midi_open_result) { .midi = midi, .error = 0 }; 29 | } 30 | 31 | // Midi_read reads bytes from the provided MIDI connection. 32 | ssize_t Midi_read(Midi midi, char *buffer, size_t buffer_size) { 33 | assert(midi); 34 | assert(midi->in); 35 | return snd_rawmidi_read(midi->in, (void *) buffer, buffer_size); 36 | } 37 | 38 | // Midi_write writes bytes to the provided MIDI connection. 39 | ssize_t Midi_write(Midi midi, const char *buffer, size_t buffer_size) { 40 | assert(midi); 41 | assert(midi->out); 42 | return snd_rawmidi_write(midi->out, (void *) buffer, buffer_size); 43 | } 44 | 45 | // Midi_close closes a MIDI connection. 46 | int Midi_close(Midi midi) { 47 | assert(midi); 48 | assert(midi->in); 49 | assert(midi->out); 50 | 51 | int inrc, outrc; 52 | 53 | inrc = snd_rawmidi_close(midi->in); 54 | outrc = snd_rawmidi_close(midi->out); 55 | 56 | if (inrc != 0) { 57 | return inrc; 58 | } 59 | return outrc; 60 | } 61 | -------------------------------------------------------------------------------- /midi_linux.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include "midi_linux.h" 7 | // #cgo linux LDFLAGS: -lasound 8 | import "C" 9 | 10 | import ( 11 | "fmt" 12 | "unsafe" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // Device provides an interface for MIDI devices. 18 | type Device struct { 19 | ID string 20 | Name string 21 | QueueSize int 22 | Type DeviceType 23 | 24 | conn C.Midi 25 | } 26 | 27 | // Open opens a MIDI device. 28 | func (d *Device) Open() error { 29 | var ( 30 | id = C.CString(d.ID) 31 | result = C.Midi_open(id) 32 | ) 33 | defer C.free(unsafe.Pointer(id)) 34 | 35 | if result.error != 0 { 36 | return errors.Errorf("error opening device %d", result.error) 37 | } 38 | d.conn = result.midi 39 | return nil 40 | } 41 | 42 | // Close closes the MIDI connection. 43 | func (d *Device) Close() error { 44 | _, err := C.Midi_close(d.conn) 45 | return err 46 | } 47 | 48 | // Packets returns a read-only channel that emits packets. 49 | func (d *Device) Packets() (<-chan []Packet, error) { 50 | var ( 51 | buf = make([]byte, 3) 52 | ch = make(chan []Packet, d.QueueSize) 53 | ) 54 | go func() { 55 | for { 56 | if _, err := d.Read(buf); err != nil { 57 | ch <- []Packet{ 58 | {Err: err}, 59 | } 60 | return 61 | } 62 | ch <- []Packet{ 63 | { 64 | Data: [3]byte{buf[0], buf[1], buf[2]}, 65 | }, 66 | } 67 | } 68 | }() 69 | return ch, nil 70 | } 71 | 72 | // Read reads data from a MIDI device. 73 | // Note that this method is only available on Linux. 74 | func (d *Device) Read(buf []byte) (int, error) { 75 | cbuf := make([]C.char, len(buf)) 76 | n, err := C.Midi_read(d.conn, &cbuf[0], C.size_t(len(buf))) 77 | for i := C.ssize_t(0); i < n; i++ { 78 | buf[i] = byte(cbuf[i]) 79 | } 80 | return int(n), err 81 | } 82 | 83 | // Write writes data to a MIDI device. 84 | func (d *Device) Write(buf []byte) (int, error) { 85 | cs := C.CString(string(buf)) 86 | n, err := C.Midi_write(d.conn, cs, C.size_t(len(buf))) 87 | C.free(unsafe.Pointer(cs)) 88 | return int(n), err 89 | } 90 | 91 | // Devices returns a list of devices. 92 | func Devices() ([]*Device, error) { 93 | var card C.int = -1 94 | 95 | if rc := C.snd_card_next(&card); rc != 0 { 96 | return nil, alsaMidiError(rc) 97 | } 98 | if card < 0 { 99 | return nil, errors.New("no sound card found") 100 | } 101 | devices := []*Device{} 102 | 103 | for { 104 | cardDevices, err := getCardDevices(card) 105 | if err != nil { 106 | return nil, err 107 | } 108 | devices = append(devices, cardDevices...) 109 | 110 | if rc := C.snd_card_next(&card); rc != 0 { 111 | return nil, alsaMidiError(rc) 112 | } 113 | if card < 0 { 114 | break 115 | } 116 | } 117 | return devices, nil 118 | } 119 | 120 | func getCardDevices(card C.int) ([]*Device, error) { 121 | var ( 122 | ctl *C.snd_ctl_t 123 | name = C.CString(fmt.Sprintf("hw:%d", card)) 124 | ) 125 | defer C.free(unsafe.Pointer(name)) 126 | 127 | if rc := C.snd_ctl_open(&ctl, name, 0); rc != 0 { 128 | return nil, alsaMidiError(rc) 129 | } 130 | var ( 131 | cardDevices = []*Device{} 132 | device C.int = -1 133 | ) 134 | for { 135 | if rc := C.snd_ctl_rawmidi_next_device(ctl, &device); rc != 0 { 136 | return nil, alsaMidiError(rc) 137 | } 138 | if device < 0 { 139 | break 140 | } 141 | deviceDevices, err := getDeviceDevices(ctl, card, C.uint(device)) 142 | if err != nil { 143 | return nil, err 144 | } 145 | cardDevices = append(cardDevices, deviceDevices...) 146 | } 147 | if rc := C.snd_ctl_close(ctl); rc != 0 { 148 | return nil, alsaMidiError(rc) 149 | } 150 | return cardDevices, nil 151 | } 152 | 153 | func getDeviceDevices(ctl *C.snd_ctl_t, card C.int, device C.uint) ([]*Device, error) { 154 | var info *C.snd_rawmidi_info_t 155 | C.snd_rawmidi_info_malloc(&info) 156 | C.snd_rawmidi_info_set_device(info, device) 157 | 158 | defer C.snd_rawmidi_info_free(info) 159 | 160 | // Get inputs. 161 | var subsIn C.uint 162 | C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_INPUT) 163 | if rc := C.snd_ctl_rawmidi_info(ctl, info); rc != 0 { 164 | return nil, alsaMidiError(rc) 165 | } 166 | subsIn = C.snd_rawmidi_info_get_subdevices_count(info) 167 | 168 | // Get outputs. 169 | var subsOut C.uint 170 | C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_OUTPUT) 171 | if rc := C.snd_ctl_rawmidi_info(ctl, info); rc != 0 { 172 | return nil, alsaMidiError(rc) 173 | } 174 | subsOut = C.snd_rawmidi_info_get_subdevices_count(info) 175 | 176 | // List subdevices. 177 | var subs C.uint 178 | if subsIn > subsOut { 179 | subs = subsIn 180 | } else { 181 | subs = subsOut 182 | } 183 | if subs == C.uint(0) { 184 | return nil, errors.New("no streams") 185 | } 186 | devices := []*Device{} 187 | 188 | for sub := C.uint(0); sub < subs; sub++ { 189 | subDevice, err := getSubdevice(ctl, info, card, device, sub, subsIn, subsOut) 190 | if err != nil { 191 | return nil, err 192 | } 193 | devices = append(devices, subDevice) 194 | } 195 | return devices, nil 196 | } 197 | 198 | func getSubdevice(ctl *C.snd_ctl_t, info *C.snd_rawmidi_info_t, card C.int, device, sub, subsIn, subsOut C.uint) (*Device, error) { 199 | if sub < subsIn { 200 | C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_INPUT) 201 | } else { 202 | C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_OUTPUT) 203 | } 204 | C.snd_rawmidi_info_set_subdevice(info, sub) 205 | if rc := C.snd_ctl_rawmidi_info(ctl, info); rc != 0 { 206 | return nil, alsaMidiError(rc) 207 | } 208 | var ( 209 | name = C.GoString(C.snd_rawmidi_info_get_name(info)) 210 | subName = C.GoString(C.snd_rawmidi_info_get_subdevice_name(info)) 211 | ) 212 | var dt DeviceType 213 | if sub < subsIn && sub >= subsOut { 214 | dt = DeviceInput 215 | } else if sub >= subsIn && sub < subsOut { 216 | dt = DeviceOutput 217 | } else { 218 | dt = DeviceDuplex 219 | } 220 | if sub == 0 && len(subName) > 0 && subName[0] == 0 { 221 | return &Device{ 222 | ID: fmt.Sprintf("hw:%d,%d", card, device), 223 | Name: name, 224 | Type: dt, 225 | }, nil 226 | } 227 | return &Device{ 228 | ID: fmt.Sprintf("hw:%d,%d,%d", card, device, sub), 229 | Name: subName, 230 | Type: dt, 231 | }, nil 232 | } 233 | 234 | func alsaMidiError(code C.int) error { 235 | if code == C.int(0) { 236 | return nil 237 | } 238 | return errors.New(C.GoString(C.snd_strerror(code))) 239 | } 240 | -------------------------------------------------------------------------------- /midi_linux.h: -------------------------------------------------------------------------------- 1 | // +build cgo 2 | #ifndef MIDI_H 3 | #define MIDI_H 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | // Midi represents a connection to a MIDI device. 10 | typedef struct Midi *Midi; 11 | 12 | // Midi_open_result enables us to return both the Midi instance and an error from Midi_open. 13 | typedef struct Midi_open_result { 14 | Midi midi; 15 | int error; 16 | } Midi_open_result; 17 | 18 | // Midi_open opens a MIDI connection to the specified device. 19 | Midi_open_result Midi_open(const char *name); 20 | 21 | // Midi_read reads bytes from the provided MIDI connection. 22 | ssize_t Midi_read(Midi midi, char *buffer, size_t buffer_size); 23 | 24 | // Midi_write writes bytes to the provided MIDI connection. 25 | ssize_t Midi_write(Midi midi, const char *buffer, size_t buffer_size); 26 | 27 | // Midi_close closes a MIDI connection. 28 | int Midi_close(Midi midi); 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /midi_test.go: -------------------------------------------------------------------------------- 1 | package midi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetMessageType(t *testing.T) { 8 | for _, tc := range []struct { 9 | Expect int 10 | Input Packet 11 | Name string 12 | }{ 13 | { 14 | Expect: MessageTypeNoteOn, 15 | Input: Packet{Data: [3]uint8{0x90, 0x4f, 0x16}}, 16 | Name: "Note On message type", 17 | }, 18 | { 19 | Expect: MessageTypeNoteOff, 20 | Input: Packet{Data: [3]uint8{0x80, 0x4f, 0x0}}, 21 | Name: "Note Off message type", 22 | }, 23 | } { 24 | if expect, got := tc.Expect, GetMessageType(tc.Input); expect != got { 25 | t.Fatalf("%s: expected %d, got %d", tc.Name, expect, got) 26 | } 27 | } 28 | } 29 | --------------------------------------------------------------------------------