diff --git a/launchpad_test.go b/launchpad_test.go index 46590f6..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,0,0", - QueueSize: 0, - } + device := &Device{ID: "hw:0"} if err := device.Open(); err != nil { t.Fatal(err) } diff --git a/list_test.go b/list_test.go new file mode 100644 index 0000000..e13bf10 --- /dev/null +++ b/list_test.go @@ -0,0 +1,19 @@ +package midi + +import ( + "fmt" + "testing" +) + +func TestDevices(t *testing.T) { + devices, err := Devices() + if err != nil { + t.Fatal(err) + } + for i, d := range devices { + if d == nil { + continue + } + fmt.Printf("device %d: %#v\n", i, *d) + } +} 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..2eef685 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" @@ -11,6 +12,7 @@ import "C" import ( "fmt" "os" + "unsafe" "github.com/pkg/errors" ) @@ -18,18 +20,33 @@ 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) + ) + defer C.free(unsafe.Pointer(id)) + if result.error != 0 { return errors.Errorf("error opening device %d", result.error) } @@ -75,6 +92,160 @@ 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 } + +type Stream struct { + Name string +} + +func Devices() ([]*Device, error) { + var card C.int = -1 + + if rc := C.snd_card_next(&card); rc != 0 { + return nil, alsaMidiError(rc) + } + if card < 0 { + return nil, errors.New("no sound card found") + } + devices := []*Device{} + + for { + cardDevices, err := getCardDevices(card) + if err != nil { + return nil, err + } + if rc := C.snd_card_next(&card); rc != 0 { + return nil, alsaMidiError(rc) + } + if card < 0 { + break + } + devices = append(devices, cardDevices...) + } + return devices, nil +} + +func getCardDevices(card C.int) ([]*Device, error) { + var ( + 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) + } + 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 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) + + 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) + 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{} + + 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 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 nil, 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))) +}