Alsa Sound Kit

The Alsa Sound Kit (ASK) provides Objective-C wrappers around ALSA MIDI devices and ALSA PCM (sound) devices. The Alsa Sound Kit depends on libasound.so and not much else except Obj-C and Foundation. The Alsa Sound Kit can be used without the Mclaren Synth Kit.

A dependency graph is shown below.

MSK on top of ASK on top of LIBASOUND

The purpose of the Alsa Sound Kit is to make it easier to use ALSA MIDI and Sound devices by doing more than wrapping the C-level ALSA functions. The Alsa Sound Kit provides subroutines for enumerating the devices in your system and for monitoring their status. The Alsa Sound Kit relieves you (the programmer) from managing threads. Instead, you set up your program to respond to MIDI events on a dispatch queue, or to respond to Audio events in a block.

ALSA MIDI System Overview

ALSA can expose MIDI devices as either "Sequencer" objects or "Raw MIDI" objects. Raw MIDI exposes the MIDI devices of sound cards and and sends and receives raw MIDI byte sequences.

Sequencer objects are a more abstract representation of the MIDI system. At the Sequencer level, the MIDI system consists of a collection of "clients". A "client" has one or more "ports". Interconnections between "ports" are made to describe a MIDI processing graph.

ASK clients and ports

On Linux, the aconnect commands can help you about the connections between your MIDI Sequencer clients. To list the clients on your system try the command below.

$ aconnect -i -o -l
client 0: 'System' [type=kernel]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=kernel]
    0 'Midi Through Port-0'
client 24: 'Launchkey 25' [type=kernel,card=2]
    0 'Launchkey 25 MIDI 1'
    1 'Launchkey 25 MIDI 2'
client 130: 'Virtual Keyboard' [type=user,pid=77517]
    0 'Virtual Keyboard'

All systems have "client 0" and client 14". Client 0 is a special System Client that announces events about the MIDI system itself. It announces the insertion and deletion of new devices in the system. Client 14 is another special client that is a MIDI pass through.

On our system, we are running a GUI "Virtual Keyboard" program called vkeybd that is exposed as client 130. We have also plugged in a USB keyboard (a Novation LaunchKey 25) and that is exposed as client 24.

The aseqdump command on Linux is a command line MIDI monitor. Use it to watch the events flowing through a specific port on a client. For instance, you can see the events produced by a MIDI USB keyboard as shown below. In this example, we played "C-D-E-F-G" on the keyboard.

$ aseqdump -p 24:0
Waiting for data. Press Ctrl+C to end.
Source  Event                  Ch  Data
 24:0   Note on                 0, note 60, velocity 33
 24:0   Note off                0, note 60, velocity 49
 24:0   Note on                 0, note 62, velocity 38
 24:0   Note on                 0, note 64, velocity 37
 24:0   Note off                0, note 62, velocity 38
 24:0   Note on                 0, note 65, velocity 29
 24:0   Note off                0, note 64, velocity 43
 24:0   Note off                0, note 65, velocity 68
 24:0   Note on                 0, note 67, velocity 48
 24:0   Note off                0, note 67, velocity 76

ALSA MIDI Sequencer devices can also be referred to by name, or by partial name. Instead of referencing client "24" we can obtain the same effect by using the client name "Launchkey 25" or just a unique client-name prefix like "Launch". Note that because there are spaces in the name, you need to use the correct quoting.

$ aseqdump -p 'Launchkey 25:0'
Waiting for data. Press Ctrl+C to end.
Source  Event                  Ch  Data
 24:0   Note on                 0, note 60, velocity 17
 24:0   Note off                0, note 60, velocity 56
 24:0   Note on                 0, note 62, velocity 45
 24:0   Note off                0, note 62, velocity 74

$ aseqdump -p Launch:0
Waiting for data. Press Ctrl+C to end.
Source  Event                  Ch  Data
 24:0   Note on                 0, note 60, velocity 60
 24:0   Note off                0, note 60, velocity 45
 24:0   Note on                 0, note 62, velocity 34
 24:0   Note off                0, note 62, velocity 74

The MIDI "System Client" (client 0) is special. Instead of handling note events, this sequencer handles timing events and events about the system on its Port 1. Run the command below and unplug the USB MIDI Keyboard and then plug it back in. The System Client announces what is happening. In the example below you see the following thing: the "Port Subscription" is the aseqdump command itself registering with client-0:port-0. The rest of the output is information about Client 24 (our Launchkey 25).

$ aseqdump -p 0:1
Waiting for data. Press Ctrl+C to end.
Source  Event                  Ch  Data
  0:1   Port subscribed            0:1 -> 128:0
  0:1   Port exit                  24:0
  0:1   Port exit                  24:1
  0:1   Client exit                client 24
  0:1   Client start               client 24
  0:1   Port start                 24:0
  0:1   Port start                 24:1

The aconnect command is usually used to make connections between clients and ports. We can explore its use with just a keyboard and the aseqdump command to illustrate how it works.

First, start the aseqdump command running in one window with no arguments, like this.

$ aseqdump
Waiting for data at port 128:0. Press Ctrl+C to end.
Source  Event                  Ch  Data

We can see that a new client was created with number 128. This is a Sequencer Client created for the aseqdump command itself.

Now, in another window, make a connection from the external keyboard to Client 128.

$ aconnect 24:0 128:0

In the aseqdump window you should see some output if you play a few notes on the keyboard.

 24:0   Note on                 0, note 60, velocity 31
 24:0   Note off                0, note 60, velocity 35
 24:0   Note on                 0, note 62, velocity 50
 24:0   Note off                0, note 62, velocity 72

Now, "disconnect" the connection you just made.

$ aconnect -d 24:0 128:0

In the 'aseqdump` window you will see the following message.

  0:1   Port unsubscribed          24:0 -> 128:0

Note that because you can refer to clients with their names, you can also set up the connection shown above in this way.

$ aconnect Launch:0 aseqdump:0
$ aconnect -d Launch:0 aseqdump:0

ALSA MIDI System Overview Summary

This section gave an introduction to the concepts of the ALSA MIDI Sequencer level interface using command line tools and an external sequencer. Using these tools, we explored Clients and Ports. A Sequencer Client corresponds to a running program (i.e. aseqdump) or a virtual or external keyboard. A Port is the source or destination of an event stream on a Client.

The command-line tools aconnect and aseqdump are helpful tools for exploring the state of the Linux MIDI system and for observing events flowing through it. With the concepts of Clients, Ports and Connections firmly understood, the Alsa Sound Kit MIDI system is straightforward to explain.

Note: the source code of the aconnect and aseqdump programs themselves are excellent for learning about the ALSA MIDI System. Look them up on the internet.

ASK Seq

An ASKSeq object is the Alsa Sound Kit wrapper for an ALSA Sequencer client. An ASKSeq produces MIDI events by calling a block passed to its addListener method. An ASKSeq may have many listeners.

It's easy to use an ASKSeq to mimic the operation of the aseqdump command. The program below is from examples-ask/askseqdump.m in the project folder. The alloc/init line creates a new sequencer with default options. The addListener method call adds a callback block that logs the received events.

int main(int argc, char *argv[])
{
    NSError *error;
    ASKSeq *seq = [[ASKSeq alloc] initWithError:&error];
    [seq addListener:^(NSArray<ASKSeqEvent*> *events) {
        for (ASKSeqEvent* e in events) {
            NSLog(@"%@", e);
        }
    }];

    [[NSRunLoop mainRunLoop] run];
}

You can use this program to monitor MIDI events. Since there is no command line handling, you have to set up a connection externally with aconnect.

$ aconnect Launch:0 askseqdump:0

Example output is shown below. (Note: if you are running VSCode there is already a build and test task configured to compile and run this program!)

$ ./askseqdump 
2020-12-28 10:08:25.900 askseqdump[85570:85587]   0:1   0 Port subscribed            24:0 -> 128:0
2020-12-28 10:08:26.884 askseqdump[85570:85587]  24:0   0 Note on                 0, note 60, velocity 43
2020-12-28 10:08:27.033 askseqdump[85570:85587]  24:0   0 Note off                0, note 60, velocity 44
2020-12-28 10:08:27.104 askseqdump[85570:85587]  24:0   0 Note on                 0, note 62, velocity 56
2020-12-28 10:08:27.262 askseqdump[85570:85587]  24:0   0 Note off                0, note 62, velocity 42
2020-12-28 10:08:27.287 askseqdump[85570:85587]  24:0   0 Note on                 0, note 64, velocity 77
2020-12-28 10:08:27.382 askseqdump[85570:85587]  24:0   0 Note off                0, note 64, velocity 65
^C

There are a couple of interesting things about this program. A new sequencer client is created with default options that allow it to process events bidirectionally (more on that later) and with a default name (that we will change). The sequencer is also created with a default port and a default ALSA timing queue.

Behind the scenes, the SEQ file descriptor is registered as a new dispatch source. A shared dispatch queue named "midi" has been created as a high-priority queue for processing MIDI events. The handler for the dispatch sources is a block that wraps each of the ALSA low-level C events in an Objective-C object called an ASKSEqEvent. The list of events produced at one time tick are gathered into an NSArray, and this is what is presented to our "Listener" block. Automatic Reference Counting (ARC) means our program does not need to manage deallocation or freeing of MIDI event objects.

the Objective-C objects of the Alsa Sound Kit have useful description methods defined. When an ASKSeqEvent is printed using NSLog, this description is printed. As a result, writing a MIDI monitor is pretty easy using the Alsa Sound Kit.

Configuring an ASK Seq - ASK Seq Options

The default way that an ASKSeq is configured is through an ASKSeqOptions object. This object provides default values for the following things:

  • sequencer name - a C string
  • sequencer_type - ALSA "default" type
  • sequencer_streams - DUPLEX
  • sequencer_mode - NONBLOCK
  • port_name - a C string
  • port_caps - READ and WRITE
  • port_type - GENERIC
  • queue_name - a C string
  • queue_temp - 60 BPM
  • queue_resolution - 1000 ppq

To change any of these from the default, allocate a new ASKSeqOptions object and override the parameter. Then open up the ASKSeq using the initWithOptions:error: method.

In the following, we've modified the program above to be more sensible.

    ASKSeqOptions *options = [[ASKSeqOptions alloc] init];
    options->_sequencer_name = "askseqdump";

    NSError *error;
    ASKSeq *seq = [[ASKSeq alloc] initWithOptions:options error:&error];

The other thing that it oftentimes makes sense to override is the default "Port" name and its capabilities (READ or WRITE, etc).

Making Connections

The connectFrom:port:error: and connectTo:port:error: methods of an ASKSeq allow it to connect from or to another client at a specified port. We could modify our MIDI dumping program above to listen for events from the external keyboard at 24:0 as shown here.

    ASKSeq *seq = [[ASKSeq alloc] initWithOptions:options error:&error];

    [seq connectFrom:24 port:0 error:&error];  // create a connection!

    [seq addListener:^(NSArray<ASKSeqEvent*> *events) {
        ...
    }];

There is a useful helper for parsing a MIDI sequencer address from a string. This is the parseAddress:error: method of the ASKSeq. This method taks an NSString as an argument. Using it, we could modify our MIDI dumping program to listen for events from the Launchkey keyboard like this. Note: the parseAddress:error: method understands client-name abbreviations because it uses snd_seq_parse_address under the hood.

    ASKSeq *seq = [[ASKSeq alloc] initWithOptions:options error:&error];

    ASKSeqAddr *addr = [seq parseAddress:@"Launch:0" error:&error]; // unique prefix of our keyboard
    [seq connectFrom:addr.client port:addr.port error:&error]; // .client and .port properties

    [seq addListener:^(NSArray<ASKSeqEvent*> *events) {
        ...
    }];

ASK Seq Event

The ALSA Seq subsystem communicates MIDI events by a rich structure defined by the type snd_seq_event_t. In working with ALSA Sequencer events it is eventually necessary to become familar with this structure definition in /usr/include/alsa/seq_event.h.

Note: previous versions of the Alsa Sound Kit attempted to abstract the use of the snd_seq_event_t structure through Objective-C properties and getters and setters. The result was complicated and did not add much value. We decided to eliminate property abstraction for the time being.

When Alsa Sound Kit receives a snd_seq_event_t from ALSA, it performs a value copy of the struct into an Objective-C object called ASKSEqEvent. Its declaration is shown here. In this way, the MIDI event data is managed by the Objective-C memory manager.

@interface ASKSeqEvent : NSObject {
  NSData *_data; // backing store for ext (sysex) data
@public
  snd_seq_event_t _ev;
}

@property (nonatomic, readwrite, getter=getExt, setter=setExt:) NSData *ext; // get and set ext data
@end

For SysEx data, the Alsa Sound Kit helps mapping the bytes of an NSData object to and from the ev->data.ext.ptr.

Handling Received Events

The example program minisynth1.m decodes MIDI events to construct sounds. In general, a loop handling received MIDI events will look something like the following. Each different type of event uses the union fields of the struct in a different way.

[seq addListener:^(NSArray<ASKSeqEvent*> *events) {
      for (ASKSeqEvent *e in events) {

        if (e->_ev.type == SND_SEQ_EVENT_NOTEON) {
          uint8_t chan = e->_ev.data.note.channel;
          uint8_t note = e->_ev.data.note.note;
          uint8_t vel = e->_ev.data.note.velocity;

          // ... do something
        }

        if (e->_ev.type == SND_SEQ_EVENT_NOTEOFF) {
          uint8_t chan = e->_ev.data.note.channel;
          uint8_t note = e->_ev.data.note.note;
          uint8_t vel = e->_ev.data.note.velocity;

          // ... do something
        }
      }
}];

Creating and Sending an Event

The opposite of receiving an event is creating an event. To create a new note event, allocate a blank ASKSEqEvent and then fill it out. The following snippet creates a NOTEON event that will be sent to all subscribers of our client.

  ASKSeqEvent *e = [[ASKSeqEvent alloc] init];
  e->_ev.type = SND_SEQ_EVENT_NOTEON;
  snd_seq_ev_set_subs(&(e->_ev)); // all subscribers
  e->_ev.dest.port = 0;
  e->_ev.data.note.channel = chan;
  e->_ev.data.note.note = note;
  e->_ev.data.note.velocity = vel;
  e->_ev.queue = SND_SEQ_QUEUE_DIRECT; // no scheduling

To send the event, use the outputDirect: method of an ASKSEq.

  [seq outputDirect:e];

To create a SysEx message, put the SysEx bytes in an NSData* and attach it to the ext field of the ASKSeqEvent before filling out the rest of the event and sending.

  ASKSeqEvent *e = [[ASKSeqEvent alloc] init];
  e.ext = sysex;

  e->_ev.type = SND_SEQ_EVENT_SYSEX;
  snd_seq_ev_set_subs(&(e->_ev)); // all subscribers
  e->_ev.dest.port = 0;
  e->_ev.queue = SND_SEQ_QUEUE_DIRECT; // no scheduling

  [seq outputDirect:e];

Listing Available Seq Clients and Ports

A special utility class called ASKSeqList is provided to list the available clients in the system. This class is pretty smart: when instantiated, it creates the current list of clients in the system, and then it registers for System ANNOUNCE events to be notified of clients and ports disconnecting and connecting. The class also allows for user notifications.

Each Client in the system is described by an ASKSeqClientInfo (a simple wrapper around an ALSA snd_seq_client_info_t). Each Port in the system is described by an ASKSeqPortInfo (a simple wrapper around an ALSA snd_seq_client_info_t).

Given an existing ASKSeq, a new ASKSeqList can be created as shown below. The clientinfos and portinfos are available directy as properties of the ASKSeqList.

  ASKSeqList *list = [[ASKSeqList alloc] initWithSeq:seq];
  for (ASKSeqClientInfo *c in list.clientinfos) {
    NSLog(@"%@", c);
  }
$ ./askseqlist 
2020-12-28 13:36:28.381 askseqlist[89989:89989] CLIENT:0 Name:System Type:2
2020-12-28 13:36:28.381 askseqlist[89989:89989] CLIENT:14 Name:Midi Through Type:2
2020-12-28 13:36:28.381 askseqlist[89989:89989] CLIENT:24 Name:Launchkey 25 Type:2
2020-12-28 13:36:28.381 askseqlist[89989:89989] CLIENT:128 Name:askseqdump Type:1
2020-12-28 13:36:28.381 askseqlist[89989:89989] CLIENT:130 Name:Virtual Keyboard Type:1

It is also possible to list all of the ports in the system.

  for (ASKSeqPortInfo *p in list.portinfos) {
    NSLog(@"%@", p);
  }

The snippet above produces the following on our system.

$ ./askseqlist 
2020-12-28 13:38:11.839 askseqlist[90222:90222] PORT Client:0 Port:0 Name:Timer cap:0 type:0 R:0 W:0
2020-12-28 13:38:11.839 askseqlist[90222:90222] PORT Client:0 Port:1 Name:Announce cap:0 type:0 R:0 W:0
2020-12-28 13:38:11.839 askseqlist[90222:90222] PORT Client:14 Port:0 Name:Midi Through Port-0 cap:655362 type:655362 R:0 W:0
2020-12-28 13:38:11.839 askseqlist[90222:90222] PORT Client:24 Port:0 Name:Launchkey 25 MIDI 1 cap:589826 type:589826 R:0 W:0
2020-12-28 13:38:11.839 askseqlist[90222:90222] PORT Client:24 Port:1 Name:Launchkey 25 MIDI 2 cap:589826 type:589826 R:0 W:0
2020-12-28 13:38:11.839 askseqlist[90222:90222] PORT Client:128 Port:0 Name:__port__ cap:2 type:2 R:0 W:0
2020-12-28 13:38:11.839 askseqlist[90222:90222] PORT Client:130 Port:0 Name:Virtual Keyboard cap:1048578 type:1048578 R:0 W:0

The ASKSeqList also keeps track of changes to the system and modifies clientinfos and portinfos as appropriate. Your code can register for callbacks to be notified of changes. The code below shows how.

   [list onClientAdded:^(ASKSeqClientInfo* c) {
        NSLog(@"Client Added - %@", c);
    }];

    [list onClientDeleted:^(ASKSeqClientInfo* c) {
        NSLog(@"Client Deleted - %@", c);
    }];

    [list onPortAdded:^(ASKSeqPortInfo* p) {
        NSLog(@"Port Added - %@", p);
    }];

    [list onPortDeleted:^(ASKSeqPortInfo* p) {
        NSLog(@"Port Deleted - %@", p);
    }];

If we compile and run this on our system, and then unplug the Launchkey keyboard and plug it back in, we see the following output.

2020-12-28 13:46:35.994 askseqlist[90803:90805] Port Deleted - PORT Client:24 Port:0 Name:Launchkey 25 MIDI 1 cap:589826 type:589826 R:0 W:0
2020-12-28 13:46:35.994 askseqlist[90803:90805] Port Deleted - PORT Client:24 Port:1 Name:Launchkey 25 MIDI 2 cap:589826 type:589826 R:0 W:0
2020-12-28 13:46:35.994 askseqlist[90803:90805] Client Deleted - CLIENT:24 Name:Launchkey 25 Type:2
2020-12-28 13:46:38.973 askseqlist[90803:90805] Client Added - CLIENT:24 Name:Launchkey 25 Type:2
2020-12-28 13:46:38.973 askseqlist[90803:90805] Port Added - PORT Client:24 Port:0 Name:Launchkey 25 MIDI 1 cap:589826 type:589826 R:0 W:0
2020-12-28 13:46:38.973 askseqlist[90803:90805] Port Added - PORT Client:24 Port:1 Name:Launchkey 25 MIDI 2 cap:589826 type:589826 R:0 W:0

McLaren Labs sound software (for instance, the mclaren80 synthesizer) uses this capability with GUI code to keep the list of devices presented in a GUI dropdown list synchronized with the current state of the system even as new USB keyboards are plugged in.

Summary

This chapter described the features of the MIDI subsystem of the Alsa Sound Kit ("ASK"). We described how Clients and Ports represent the devices and connections in a Linux system and how you can view and manipulate these with the aconnect and aseqdump commands.

We then showed how Clients and Ports and Connections can be managed in Objective-C code. An ASKSeq is an object representing a new Sequencer Client. It is created with a default Port and a pre-made queue for handling events. An ASKSeqOptions object can be used to customize a basic sequencer client with a specific client name or port name.

We showed how to make and break connections with the connectFrom:, connectTo:, disconnectFrom: and disconnectTo:methods.

MIDI events were introduced and we showed how to examine incoming events and how to construct new events for sending.

Lastly, we showed how to write code that enumerates the Clients and Ports in the system and how to be notified about changing system topology as MIDI devices are added and deleted.