From d50a32b1725ae42044c84f02a53c69004926bd93 Mon Sep 17 00:00:00 2001 From: Brian Sorahan Date: Sun, 19 Feb 2017 17:04:54 -0600 Subject: [PATCH 1/4] first pass at devices list on linux --- launchpad_test.go | 2 +- list_test.go | 14 ++++++ midi_linux.c | 1 - midi_linux.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 list_test.go diff --git a/launchpad_test.go b/launchpad_test.go index 46590f6..c017edf 100644 --- a/launchpad_test.go +++ b/launchpad_test.go @@ -16,7 +16,7 @@ func TestLaunchpad(t *testing.T) { // t.SkipNow() device := &Device{ - Name: "hw:0,0,0", + Name: "hw:0", QueueSize: 0, } if err := device.Open(); err != nil { diff --git a/list_test.go b/list_test.go new file mode 100644 index 0000000..7828336 --- /dev/null +++ b/list_test.go @@ -0,0 +1,14 @@ +package midi + +import ( + "fmt" + "testing" +) + +func TestDevices(t *testing.T) { + devices, err := Devices() + if err != nil { + t.Fatal(err) + } + fmt.Printf("devices %#v\n", devices) +} diff --git a/midi_linux.c b/midi_linux.c index ce3f8a7..645a226 100644 --- a/midi_linux.c +++ b/midi_linux.c @@ -1,6 +1,5 @@ // +build cgo #include -#include #include #include diff --git a/midi_linux.go b/midi_linux.go index 99be9c6..784753c 100644 --- a/midi_linux.go +++ b/midi_linux.go @@ -2,6 +2,7 @@ // package for talking to midi devices in Go. package midi +// #include // #include // #include // #include "midi_linux.h" @@ -78,3 +79,111 @@ func (d *Device) Write(buf []byte) (int, error) { n, err := C.Midi_write(d.conn, C.CString(string(buf)), C.size_t(len(buf))) return int(n), err } + +type Stream struct { + Name string +} + +type DeviceInfo struct { + ID string + Name string + Inputs []Stream + Outputs []Stream +} + +func Devices() ([]DeviceInfo, error) { + var ( + seqp *C.snd_seq_t + cinfo *C.snd_seq_client_info_t + pinfo *C.snd_seq_port_info_t + ) + if rc := C.snd_seq_open(&seqp, C.CString("default"), C.SND_SEQ_OPEN_DUPLEX, 0); rc != 0 { + return nil, alsaMidiError(rc) + } + if rc := C.snd_seq_client_info_malloc(&cinfo); rc != 0 { + return nil, alsaMidiError(rc) + } + + if rc := C.snd_seq_port_info_malloc(&pinfo); rc != 0 { + return nil, alsaMidiError(rc) + } + C.snd_seq_client_info_set_client(cinfo, -1) + + devices := []DeviceInfo{} + + for C.snd_seq_query_next_client(seqp, cinfo) == 0 { + C.snd_seq_port_info_set_client(pinfo, C.snd_seq_client_info_get_client(cinfo)) + C.snd_seq_port_info_set_port(pinfo, -1) + + if clientID := C.snd_seq_port_info_get_client(pinfo); isSystemClient(clientID) { + continue // ignore Timer and Announce ports on client 0 + } + device := DeviceInfo{ + Name: C.GoString(C.snd_seq_client_info_get_name(cinfo)), + } + if card := C.snd_seq_client_info_get_card(cinfo); card >= 0 { + device.ID = fmt.Sprintf("hw:%d", card) + } + for C.snd_seq_query_next_port(seqp, pinfo) == 0 { + if portType := C.snd_seq_port_info_get_type(pinfo); !isMidiPort(portType) { + fmt.Printf("portType is not a midi port %d\n", portType) + continue // Not a MIDI port. + } + var ( + caps = C.snd_seq_port_info_get_capability(pinfo) + portName = C.GoString(C.snd_seq_port_info_get_name(pinfo)) + stream = Stream{Name: portName} + ) + if isDuplexPort(caps) { + // Add duplex stream. + device.Inputs = append(device.Inputs, stream) + device.Outputs = append(device.Outputs, stream) + } else if isWritePort(caps) { + // Add write stream. + device.Outputs = append(device.Outputs, stream) + } else if isReadPort(caps) { + // Add read stream. + device.Inputs = append(device.Inputs, stream) + } + } + devices = append(devices, device) + } + return devices, nil +} + +func isSystemClient(clientID C.int) bool { + return clientID == C.SND_SEQ_CLIENT_SYSTEM +} + +func isSystemPort(portID C.int) bool { + return portID == C.SND_SEQ_PORT_SYSTEM_ANNOUNCE || portID == C.SND_SEQ_PORT_SYSTEM_TIMER +} + +func isMidiPort(portType C.uint) bool { + return (portType&C.SND_SEQ_PORT_TYPE_MIDI_GENERIC != 0) || + (portType&C.SND_SEQ_PORT_TYPE_MIDI_GM != 0) || + (portType&C.SND_SEQ_PORT_TYPE_MIDI_GS != 0) || + (portType&C.SND_SEQ_PORT_TYPE_MIDI_XG != 0) || + (portType&C.SND_SEQ_PORT_TYPE_MIDI_MT32 != 0) || + (portType&C.SND_SEQ_PORT_TYPE_MIDI_GM2 != 0) +} + +func isDuplexPort(caps C.uint) bool { + return (caps & C.SND_SEQ_PORT_CAP_DUPLEX) != 0 +} + +func isWritePort(caps C.uint) bool { + return (caps&C.SND_SEQ_PORT_CAP_WRITE != 0) || + (caps&C.SND_SEQ_PORT_CAP_SYNC_WRITE != 0) || + (caps&C.SND_SEQ_PORT_CAP_SUBS_WRITE != 0) +} + +func isReadPort(caps C.uint) bool { + return (caps&C.SND_SEQ_PORT_CAP_READ != 0) || + (caps&C.SND_SEQ_PORT_CAP_SYNC_READ != 0) || + (caps&C.SND_SEQ_PORT_CAP_SUBS_READ != 0) +} + +func alsaMidiError(code C.int) error { + return errors.New(C.GoString(C.snd_strerror(code))) +} From 421d9910872be36672cd143b2ef98cfe252a38e9 Mon Sep 17 00:00:00 2001 From: Brian Sorahan Date: Wed, 22 Feb 2017 21:47:40 -0600 Subject: [PATCH 2/4] rewrote Devices func for linux --- launchpad_test.go | 5 +- midi_linux.go | 223 +++++++++++++++++++++++++++++----------------- 2 files changed, 142 insertions(+), 86 deletions(-) diff --git a/launchpad_test.go b/launchpad_test.go index c017edf..aa216c1 100644 --- a/launchpad_test.go +++ b/launchpad_test.go @@ -15,10 +15,7 @@ func TestLaunchpad(t *testing.T) { // For the launchpad MIDI reference, see https://d19ulaff0trnck.cloudfront.net/sites/default/files/novation/downloads/4080/launchpad-programmers-reference.pdf // t.SkipNow() - device := &Device{ - Name: "hw:0", - QueueSize: 0, - } + device := &Device{ID: "hw:0"} if err := device.Open(); err != nil { t.Fatal(err) } diff --git a/midi_linux.go b/midi_linux.go index 784753c..902a1bf 100644 --- a/midi_linux.go +++ b/midi_linux.go @@ -12,6 +12,7 @@ import "C" import ( "fmt" "os" + "unsafe" "github.com/pkg/errors" ) @@ -19,22 +20,36 @@ import ( // Packet is a MIDI packet. type Packet [3]byte +// DeviceType is a flag that says if a device is an input, an output, or duplex. +type DeviceType int + +const ( + DeviceInput DeviceType = iota + DeviceOutput + DeviceDuplex +) + // Device provides an interface for MIDI devices. type Device struct { + ID string Name string QueueSize int + Type DeviceType conn C.Midi - buf []byte } // Open opens a MIDI device. func (d *Device) Open() error { - result := C.Midi_open(C.CString(d.Name)) + var ( + id = C.CString(d.ID) + result = C.Midi_open(id) + ) if result.error != 0 { return errors.Errorf("error opening device %d", result.error) } d.conn = result.midi + C.free(unsafe.Pointer(id)) return nil } @@ -76,7 +91,9 @@ func (d *Device) Read(buf []byte) (int, error) { // Write writes data to a MIDI device. func (d *Device) Write(buf []byte) (int, error) { - n, err := C.Midi_write(d.conn, C.CString(string(buf)), C.size_t(len(buf))) + cs := C.CString(string(buf)) + n, err := C.Midi_write(d.conn, cs, C.size_t(len(buf))) + C.free(unsafe.Pointer(cs)) return int(n), err } @@ -84,106 +101,148 @@ type Stream struct { Name string } -type DeviceInfo struct { - ID string - Name string - Inputs []Stream - Outputs []Stream -} +func Devices() ([]Device, error) { + var card C.int = -1 -func Devices() ([]DeviceInfo, error) { - var ( - seqp *C.snd_seq_t - cinfo *C.snd_seq_client_info_t - pinfo *C.snd_seq_port_info_t - ) - if rc := C.snd_seq_open(&seqp, C.CString("default"), C.SND_SEQ_OPEN_DUPLEX, 0); rc != 0 { - return nil, alsaMidiError(rc) - } - if rc := C.snd_seq_client_info_malloc(&cinfo); rc != 0 { + if rc := C.snd_card_next(&card); rc != 0 { return nil, alsaMidiError(rc) } - - if rc := C.snd_seq_port_info_malloc(&pinfo); rc != 0 { - return nil, alsaMidiError(rc) + if card < 0 { + return nil, errors.New("no sound card found") } - C.snd_seq_client_info_set_client(cinfo, -1) - - devices := []DeviceInfo{} + devices := []Device{} - for C.snd_seq_query_next_client(seqp, cinfo) == 0 { - C.snd_seq_port_info_set_client(pinfo, C.snd_seq_client_info_get_client(cinfo)) - C.snd_seq_port_info_set_port(pinfo, -1) - - if clientID := C.snd_seq_port_info_get_client(pinfo); isSystemClient(clientID) { - continue // ignore Timer and Announce ports on client 0 - } - device := DeviceInfo{ - Name: C.GoString(C.snd_seq_client_info_get_name(cinfo)), + for { + cardDevices, err := getCardDevices(card) + if err != nil { + return nil, err } - if card := C.snd_seq_client_info_get_card(cinfo); card >= 0 { - device.ID = fmt.Sprintf("hw:%d", card) + if rc := C.snd_card_next(&card); rc != 0 { + return nil, alsaMidiError(rc) } - for C.snd_seq_query_next_port(seqp, pinfo) == 0 { - if portType := C.snd_seq_port_info_get_type(pinfo); !isMidiPort(portType) { - fmt.Printf("portType is not a midi port %d\n", portType) - continue // Not a MIDI port. - } - var ( - caps = C.snd_seq_port_info_get_capability(pinfo) - portName = C.GoString(C.snd_seq_port_info_get_name(pinfo)) - stream = Stream{Name: portName} - ) - if isDuplexPort(caps) { - // Add duplex stream. - device.Inputs = append(device.Inputs, stream) - device.Outputs = append(device.Outputs, stream) - } else if isWritePort(caps) { - // Add write stream. - device.Outputs = append(device.Outputs, stream) - } else if isReadPort(caps) { - // Add read stream. - device.Inputs = append(device.Inputs, stream) - } + if card < 0 { + break } - devices = append(devices, device) + devices = append(devices, cardDevices...) } return devices, nil } -func isSystemClient(clientID C.int) bool { - return clientID == C.SND_SEQ_CLIENT_SYSTEM -} +func getCardDevices(card C.int) ([]Device, error) { + var ( + ctl *C.snd_ctl_t + name = C.CString(fmt.Sprintf("hw:%d", card)) + ) + if rc := C.snd_ctl_open(&ctl, name, 0); rc != 0 { + return nil, alsaMidiError(rc) + } + C.free(unsafe.Pointer(name)) -func isSystemPort(portID C.int) bool { - return portID == C.SND_SEQ_PORT_SYSTEM_ANNOUNCE || portID == C.SND_SEQ_PORT_SYSTEM_TIMER + var ( + cardDevices = []Device{} + device C.int = -1 + ) + for { + if rc := C.snd_ctl_rawmidi_next_device(ctl, &device); rc != 0 { + return nil, alsaMidiError(rc) + } + if device < 0 { + break + } + deviceDevices, err := getDeviceDevices(ctl, card, C.uint(device)) + if err != nil { + return nil, err + } + cardDevices = append(cardDevices, deviceDevices...) + } + if rc := C.snd_ctl_close(ctl); rc != 0 { + return nil, alsaMidiError(rc) + } + return cardDevices, nil } -func isMidiPort(portType C.uint) bool { - return (portType&C.SND_SEQ_PORT_TYPE_MIDI_GENERIC != 0) || - (portType&C.SND_SEQ_PORT_TYPE_MIDI_GM != 0) || - (portType&C.SND_SEQ_PORT_TYPE_MIDI_GS != 0) || - (portType&C.SND_SEQ_PORT_TYPE_MIDI_XG != 0) || - (portType&C.SND_SEQ_PORT_TYPE_MIDI_MT32 != 0) || - (portType&C.SND_SEQ_PORT_TYPE_MIDI_GM2 != 0) -} +func getDeviceDevices(ctl *C.snd_ctl_t, card C.int, device C.uint) ([]Device, error) { + var info *C.snd_rawmidi_info_t + C.snd_rawmidi_info_malloc(&info) + C.snd_rawmidi_info_set_device(info, device) -func isDuplexPort(caps C.uint) bool { - return (caps & C.SND_SEQ_PORT_CAP_DUPLEX) != 0 -} + // Get inputs. + var subsIn C.uint + C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_INPUT) + if rc := C.snd_ctl_rawmidi_info(ctl, info); rc != 0 { + return nil, alsaMidiError(rc) + } + subsIn = C.snd_rawmidi_info_get_subdevices_count(info) + + // Get outputs. + var subsOut C.uint + C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_OUTPUT) + if rc := C.snd_ctl_rawmidi_info(ctl, info); rc != 0 { + return nil, alsaMidiError(rc) + } + subsOut = C.snd_rawmidi_info_get_subdevices_count(info) + + // List subdevices. + var subs C.uint + if subsIn > subsOut { + subs = subsIn + } else { + subs = subsOut + } + if subs == C.uint(0) { + return nil, errors.New("no streams") + } + devices := []Device{} -func isWritePort(caps C.uint) bool { - return (caps&C.SND_SEQ_PORT_CAP_WRITE != 0) || - (caps&C.SND_SEQ_PORT_CAP_SYNC_WRITE != 0) || - (caps&C.SND_SEQ_PORT_CAP_SUBS_WRITE != 0) + for sub := C.uint(0); sub < subs; sub++ { + subDevice, err := getSubdevice(ctl, info, card, device, sub, subsIn, subsOut) + if err != nil { + return nil, err + } + devices = append(devices, subDevice) + } + return devices, nil } -func isReadPort(caps C.uint) bool { - return (caps&C.SND_SEQ_PORT_CAP_READ != 0) || - (caps&C.SND_SEQ_PORT_CAP_SYNC_READ != 0) || - (caps&C.SND_SEQ_PORT_CAP_SUBS_READ != 0) +func getSubdevice(ctl *C.snd_ctl_t, info *C.snd_rawmidi_info_t, card C.int, device, sub, subsIn, subsOut C.uint) (Device, error) { + if sub < subsIn { + C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_INPUT) + } else { + C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_OUTPUT) + } + C.snd_rawmidi_info_set_subdevice(info, sub) + if rc := C.snd_ctl_rawmidi_info(ctl, info); rc != 0 { + return Device{}, alsaMidiError(rc) + } + var ( + name = C.GoString(C.snd_rawmidi_info_get_name(info)) + subName = C.GoString(C.snd_rawmidi_info_get_subdevice_name(info)) + ) + var dt DeviceType + if sub < subsIn && sub >= subsOut { + dt = DeviceInput + } else if sub >= subsIn && sub < subsOut { + dt = DeviceOutput + } else { + dt = DeviceDuplex + } + if sub == 0 && len(subName) > 0 && subName[0] == 0 { + return Device{ + ID: fmt.Sprintf("hw:%d,%d", card, device), + Name: name, + Type: dt, + }, nil + } + return Device{ + ID: fmt.Sprintf("hw:%d,%d,%d", card, device, sub), + Name: subName, + Type: dt, + }, nil } func alsaMidiError(code C.int) error { + if code == C.int(0) { + return nil + } return errors.New(C.GoString(C.snd_strerror(code))) } From 01cb19b94274e2d985c29c0582dd612b2b04eb4b Mon Sep 17 00:00:00 2001 From: Brian Sorahan Date: Wed, 22 Feb 2017 22:05:39 -0600 Subject: [PATCH 3/4] return []*Device --- list_test.go | 7 ++++++- midi_linux.go | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/list_test.go b/list_test.go index 7828336..e13bf10 100644 --- a/list_test.go +++ b/list_test.go @@ -10,5 +10,10 @@ func TestDevices(t *testing.T) { if err != nil { t.Fatal(err) } - fmt.Printf("devices %#v\n", devices) + for i, d := range devices { + if d == nil { + continue + } + fmt.Printf("device %d: %#v\n", i, *d) + } } diff --git a/midi_linux.go b/midi_linux.go index 902a1bf..1758f39 100644 --- a/midi_linux.go +++ b/midi_linux.go @@ -101,7 +101,7 @@ type Stream struct { Name string } -func Devices() ([]Device, error) { +func Devices() ([]*Device, error) { var card C.int = -1 if rc := C.snd_card_next(&card); rc != 0 { @@ -110,7 +110,7 @@ func Devices() ([]Device, error) { if card < 0 { return nil, errors.New("no sound card found") } - devices := []Device{} + devices := []*Device{} for { cardDevices, err := getCardDevices(card) @@ -128,7 +128,7 @@ func Devices() ([]Device, error) { return devices, nil } -func getCardDevices(card C.int) ([]Device, error) { +func getCardDevices(card C.int) ([]*Device, error) { var ( ctl *C.snd_ctl_t name = C.CString(fmt.Sprintf("hw:%d", card)) @@ -139,7 +139,7 @@ func getCardDevices(card C.int) ([]Device, error) { C.free(unsafe.Pointer(name)) var ( - cardDevices = []Device{} + cardDevices = []*Device{} device C.int = -1 ) for { @@ -161,7 +161,7 @@ func getCardDevices(card C.int) ([]Device, error) { return cardDevices, nil } -func getDeviceDevices(ctl *C.snd_ctl_t, card C.int, device C.uint) ([]Device, error) { +func getDeviceDevices(ctl *C.snd_ctl_t, card C.int, device C.uint) ([]*Device, error) { var info *C.snd_rawmidi_info_t C.snd_rawmidi_info_malloc(&info) C.snd_rawmidi_info_set_device(info, device) @@ -192,7 +192,7 @@ func getDeviceDevices(ctl *C.snd_ctl_t, card C.int, device C.uint) ([]Device, er if subs == C.uint(0) { return nil, errors.New("no streams") } - devices := []Device{} + devices := []*Device{} for sub := C.uint(0); sub < subs; sub++ { subDevice, err := getSubdevice(ctl, info, card, device, sub, subsIn, subsOut) @@ -204,7 +204,7 @@ func getDeviceDevices(ctl *C.snd_ctl_t, card C.int, device C.uint) ([]Device, er return devices, nil } -func getSubdevice(ctl *C.snd_ctl_t, info *C.snd_rawmidi_info_t, card C.int, device, sub, subsIn, subsOut C.uint) (Device, error) { +func getSubdevice(ctl *C.snd_ctl_t, info *C.snd_rawmidi_info_t, card C.int, device, sub, subsIn, subsOut C.uint) (*Device, error) { if sub < subsIn { C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_INPUT) } else { @@ -212,7 +212,7 @@ func getSubdevice(ctl *C.snd_ctl_t, info *C.snd_rawmidi_info_t, card C.int, devi } C.snd_rawmidi_info_set_subdevice(info, sub) if rc := C.snd_ctl_rawmidi_info(ctl, info); rc != 0 { - return Device{}, alsaMidiError(rc) + return nil, alsaMidiError(rc) } var ( name = C.GoString(C.snd_rawmidi_info_get_name(info)) @@ -227,13 +227,13 @@ func getSubdevice(ctl *C.snd_ctl_t, info *C.snd_rawmidi_info_t, card C.int, devi dt = DeviceDuplex } if sub == 0 && len(subName) > 0 && subName[0] == 0 { - return Device{ + return &Device{ ID: fmt.Sprintf("hw:%d,%d", card, device), Name: name, Type: dt, }, nil } - return Device{ + return &Device{ ID: fmt.Sprintf("hw:%d,%d,%d", card, device, sub), Name: subName, Type: dt, From 1cf1cc2617f6774ff91edde121ad69ab5728c5c2 Mon Sep 17 00:00:00 2001 From: Brian Sorahan Date: Thu, 23 Feb 2017 11:04:51 -0600 Subject: [PATCH 4/4] fix memory leaks --- midi_linux.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/midi_linux.go b/midi_linux.go index 1758f39..2eef685 100644 --- a/midi_linux.go +++ b/midi_linux.go @@ -45,11 +45,12 @@ func (d *Device) Open() error { id = C.CString(d.ID) result = C.Midi_open(id) ) + defer C.free(unsafe.Pointer(id)) + if result.error != 0 { return errors.Errorf("error opening device %d", result.error) } d.conn = result.midi - C.free(unsafe.Pointer(id)) return nil } @@ -133,11 +134,11 @@ func getCardDevices(card C.int) ([]*Device, error) { ctl *C.snd_ctl_t name = C.CString(fmt.Sprintf("hw:%d", card)) ) + defer C.free(unsafe.Pointer(name)) + if rc := C.snd_ctl_open(&ctl, name, 0); rc != 0 { return nil, alsaMidiError(rc) } - C.free(unsafe.Pointer(name)) - var ( cardDevices = []*Device{} device C.int = -1 @@ -166,6 +167,8 @@ func getDeviceDevices(ctl *C.snd_ctl_t, card C.int, device C.uint) ([]*Device, e C.snd_rawmidi_info_malloc(&info) C.snd_rawmidi_info_set_device(info, device) + defer C.snd_rawmidi_info_free(info) + // Get inputs. var subsIn C.uint C.snd_rawmidi_info_set_stream(info, C.SND_RAWMIDI_STREAM_INPUT)