Subscription mechanism of ALSA sequencer

Takashi Iwai <tiwai@suse.de>

0. Preface

This documents explains the subscription mechanism in ALSA sequencer briefly. The information is based on the latest CVS version of ALSA driver and library (0.5.0), and most likely different from the older version (e.g. 0.4.1 series).

1. What's Subscription?

Although subscription feature is fundamental and was implemented already in the very early release of ALSA sequencer, it has not been fully documented.

Suppose a MIDI input device which sends events from a keyboard. The port associated with this device has READ capability - which means this port is readable from other ports. If a user program wants to capture events from keyboard and store them as MIDI stream, this program must subscribe itself to the MIDI port for read. Then, a connection from MIDI input port to this program is established. From this time, events from keyboard are automatically sent to this program. Timestamps will be updated according to the subscribed queue.

	MIDI input port (keyboard)
	    |
	    V
	ALSA sequencer - update timestamp
	    |
	    V
	application port

There is another subscription type for opposite direction: Suppose a MIDI sequencer program which sends events to a MIDI output device. In ALSA system, MIDI device is not opened until the associated MIDI port is accessed. Thus, in order to activate MIDI device, we have to subscribe to MIDI port for write. After this connection is established, events will be properly sent to MIDI output device.

	application port
	    |
	    V
	ALSA sequencer - events are scheduled
	    |
	    V
	MIDI output port (WaveTable etc.)

From the viewpoint of subscription, the examples above are special cases. Basically, subscription means the connection between two arbitrary ports. For example, imagine a filter application which modifies the MIDI events like program, velocity or chorus effects. This application can accept arbitrary MIDI input and send to arbitrary port, just like a Unix pipe application using stdin and stdout files. We can even connect several filter applictions which work individually in order to process the MIDI events. Subscription can be used for this purpose. The connection between ports can be done also by the "third" client. Thus, filter applications have to manage only input and output events regardless of receiver/sender addresses. This idea is similar to MidiShare connection mechanism.

	sequencer port #1
	    |
	    V
	ALSA sequencer (scheduled or real-time)
	    |
	    V
	sequencer port #2

2. More Inside the Subscription

2.1. Permissions

Each ALSA port can have following capabilties: The READ capability means that the port allows to send events to other ports, while the WRITE capability menas that the port allows to receive events from other ports. You may have noticed that meanings of READ and WRITE are permissions of the port from the viewpoint of other ports.

READ_SUBS and WRITE_SUBS are capabilities which allow read and write subscriptions, respectively. For example, the port with MIDI input device always has READ_SUBS capability, and the port with MIDI output device always has WRITE_SUBS capability together with READ and WRITE capabilities, respectively. Obviously, these flags have no influence if READ or WRITE capability is not set.

Note that these flags are not necessary if the client subscribes itself to the spcified port. For example, when a port makes READ subscription to MIDI input port, this port must have WRITE capability, but no WRITE_SUBS capability is required. Only MIDI input port must have READ_SUBS capability.

NO_EXPORT capability prohibits the subscription by the third client. If this flag is set, subscription must be done by sender or receiver client itself. It is useful to avoid unexpected disconnection. The ports which won't accept subscription should have this capability for better security.

Each ALSA client and port have "group" field. The access permission above can be defined also for the group. The "group" permission is checked only when access or subscription to "all" is refused.

2.2. Argument for ioctl

Subscription is done via ioctl with SND_SEQ_IOCTL_SUBSCRIBE_PORT. In ALSA library, this corresponds to snd_seq_subscribe_port() function. The argument has the following record:
typedef struct {
	snd_seq_addr_t sender;
	snd_seq_addr_t dest;
	int exclusive: 1;
	int realtime: 1;
	int convert_time: 1;
} snd_seq_port_subscribe_t;
The sender and dest fields are the addresses of sender and receiver ports, respectively. For example, to connect from MIDI input port to the self port, set the following values:
sender.client = MIDI_input_client;
sender.port = MIDI_input_port;
dest.client = my_client;
dest.port = my_port;
The queue value is necessary if convert_time flag is true. (See below).

If exclusive flag is true, only one connection is allowed. The succeeding subscriptions will be refused.

If convert_time is true, the timestamp through subscribed connections will be automatically converted to the current time on the specified queue. The queue must be specified at queue. If realtime is true, the timestamp is converted to timespec structure (sec/nsec). Otherwise, the timestamp is stored in tempo/tick value. This feature is useful when receiving events from MIDI input device. The event time is automatically set in the event record.

Note that both sender and dest address don't have to be the ioctl caller client (third client). In this case, however, the subscription will be refused if NO_EXPORT capability is set in sender or receiver port.

2.3. Examples of Subscription

2.3.1. Capture from keyboard

MIDI input port = 64:0, application port = 128:0, queue for timestamp = 1 with realtime stamp. The application port must have capabilty SND_SEQ_PORT_CAP_WRITE.
(..in application 128:0..)
snd_seq_port_subscribe_t subs;
memset(&subs, 0, sizeof(subs));
subs.sender.client = 64;
subs.sender.port = 0;
subs.dest.client = 128;
subs.dest.port = 0;
subs.queue = 1;
subs.time_convert = 1;
subs.realtime = 1;
snd_seq_subscribe_port(seq, &subs);

2.3.2. Output to MIDI device

MIDI output port = 65:1, application port = 128:0. The application port must have capabilty SND_SEQ_PORT_CAP_READ.
(..in application 128:0..)
snd_seq_port_subscribe_t subs;
memset(&subs, 0, sizeof(subs));
subs.sender.client = 128;
subs.sender.port = 0;
subs.dest.client = 65;
subs.dest.port = 1;
snd_seq_subscribe_port(seq, &subs);

2.3.2. Arbitrary Connection

Connect from application 128:0 to 129:0. The former must have capabilities READ and READ_SUBS, and the latter WRITE and WRITE_SUBS, respectively. The subscription is done by the third application (130:0).
(..in the third application..)
snd_seq_port_subscribe_t subs;
memset(&subs, 0, sizeof(subs));
subs.sender.client = 128;
subs.sender.port = 0;
subs.dest.client = 129;
subs.dest.port = 0;
snd_seq_subscribe_port(seq, &subs);

3. Event Processing

3.1. Addressing

Now, two ports are connected by subscription. Then how to send events?

The subscribed port doesn't have to know the exact sender address. Instead, there is a special address for subscribers, SND_SEQ_ADDRESS_SUBSCRIBERS (= 254). The sender must set this value as the destination client. Destination port is ignored.

The other values in source and destination addresses are identical with the normal event record. If the event is scheduled, proper queue and timestamp values must be set.

There is a convenient function to set the address in an event record. In order to set destination as subscribers,

void snd_seq_ev_set_subs(snd_seq_event_t *ev);

3.2. Scheduled Delivery

If we send an event at the scheduled time t (tick) on the queue Q, the sender must set both schedule queue and time in the event record. There are functions to set the scheduling queue and time stamp.
void snd_seq_ev_schedule_tick(snd_seq_event_t *ev, int q, int relative,
			      snd_seq_tick_time_t tick);
void snd_seq_ev_schedule_real(snd_seq_event_t *ev, int q, int relative,
			      snd_seq_real_time_t *real);
The former set tick value on the specified queue q, and the latter set the real-time value. If relative is non-zero, the relative time is set.

Thus, the program will become like:

snd_seq_event_t ev;

snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, port);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_schedule_tick(&ev, Q, 0, t);
...

snd_seq_event_output(seq, &ev);
...
snd_seq_flush_output(seq);  // if necessary
Of course, you can use realtime stamp, too.

3.3. Direct Delivery

If the event is sent immediately without enqueued, the sender doesn't take care of queue and timestamp. As well as the case above, there is a function to set the direct delivery.
void snd_seq_ev_set_direct(snd_seq_event_t *ev);
The program will be more simplified as:
snd_seq_event_t ev;

snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, port);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_set_direct(&ev);
...

snd_seq_output_event(seq, &ev);
snd_seq_flush_output(seq);
You should flush event soon after output event. Otherwise, the event is enqueued on output queue of ALSA library (not in the kernel!), and will be never processed until this queue becomes full.

3.4. Filter Application

A typical filter program, which receives an event and sends it immediately after some modification, will appear as following:
snd_seq_t *seq;
snd_seq_event_t *ev;

snd_seq_open(&seq, SND_SEQ_OPEN);
...

while (snd_seq_event_input(seq, &ev) >= 0) {
	//.. modify input event ..

	snd_seq_ev_set_source(ev, my_port);
	snd_seq_ev_set_subs(ev);
	snd_seq_ev_set_direct(ev);
	snd_seq_event_output(seq, &ev);
	snd_seq_flush_output(seq);
	snd_seq_free_event(ev);
}


Takashi Iwai tiwai@suse.de My ALSA Page