The sequencer core stuff only takes care of two things: scheduling events and dispatching them to the destination at the right time. All processing of MIDI events has to be done within the clients. The event can be dispatched immediately without queueing, too. The event scheduling can be done either on a MIDI tempo queue or on a wallclock-time queue.
// create a new client snd_seq_t *open_client() { snd_seq_t *handle; int err; err = snd_seq_open(&handle, "default", SND_SEQ_OPEN_INPUT, 0); if (err < 0) return NULL; snd_seq_set_client_name(handle, "My Client"); return handle; }
You'll need to know the id number of the client eventually, for example, when accessing to a certain port (see the section Subscription). The client id can be obtained by snd_seq_client_id() function.
A client can have one or more ports to communicate between other clients. A port is corresponding to the MIDI port in the case of MIDI device, but in general it is nothing but the access point between other clients. Each port may have capability flags, which specify the read/write accessbility and subscription permissions of the port. For creation of a port, call snd_seq_create_port() with the appropirate port attribute specified in snd_seq_port_info_t reocrd.
For creating a port for the normal use, there is a helper function snd_seq_create_simple_port(). An example with this function is like below.
// create a new port; return the port id // port will be writable and accept the write-subscription. int my_new_port(snd_seq_t *handle) { return snd_seq_create_simple_port(handle, "my port", SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC); }
The all scheduled output events or input events from dispatcher are stored on these pools until delivered to other clients or extracted to user space. The size of input/output pools can be changed independently. The output pool has also a room size, which is used to wake up the thread when it falls into sleep in blocking write mode.
Note that ports on the same client share the same memory pool. If a port fills the memory pool, another can't use it any more. For avoiding this, multiple clients can be used.
For chancing the pool size and the condition, access to snd_seq_client_pool_t record. There are helper functions, snd_seq_set_client_pool_output(), snd_seq_set_client_pool_output_room() and snd_seq_set_client_pool_input(), for setting the total output-pool size, the output-room size and the input-pool size, respectively.
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.
sequencer port #1 | V ALSA sequencer (scheduled or real-time) | V sequencer port #2
For the detail about subscription, see the section More inside the subscription.
All the sequencer events are stored in a sequencer event record, snd_seq_event_t type. Application can send and receive these event records to/from other clients via sequencer. An event has several stroage types according to its usage. For example, a SYSEX message is stored on the variable length event, and a large synth sample data is delivered using a user-space data pointer.
The actual record is shown in snd_seq_event_t. The type field contains the type of the event (1 byte). The flags field consists of bit flags which describe several conditions of the event (1 byte). It includes the time-stamp mode, data storage type, and scheduling prority. The tag field is an arbitrary tag. This tag can used for removing a distinct event from the event queue via snd_seq_remove_events(). The queue field is the queue id for scheduling. The source and dest fields are source and destination addresses. The data field is a union of event data.
seq_ev_queue Scheduling queue
An event can be delivered either on scheduled or direct dispatch mode. On the scheduling mode, an event is once stored on the priority queue and delivered later (or even immediately) to the destination, whereas on the direct disatch mode, an event is passed to the destination without any queue.
For a scheduled delivery, a queue to process the event must exist. Usually, a client creates its own queue by snd_seq_alloc_queue() function. Alternatively, a queue may be shared among several clients. For scheduling an event on the specified queue, a client needs to fill queue field with the preferred queue id.
Meanwhile, for dispatching an event directly, just use SND_SEQ_QUEUE_DIRECT as the target queue id. A macro snd_seq_ev_set_direct() is provided for ease and compatibility.
Note that scheduling at the current or earlier time is different from the direct dispatch mode even though the event is delivered immediately. On the former scheme, an event is once stored on priority queue, then delivered actually. Thus, it acquires a space from memory pool. On the other hand, the latter is passed without using memory pool. Although the direct dispatched event needs less memory, it means also that the event cannot be resent if the destination is unable to receive it momentarily.
The resolution of real-time value is in nano second. Since 64 bit length is required for the actual time calculation, it is represented by a structure of pair of second and nano second defined as snd_seq_real_time_t type. The song tick is defined simply as a 32 bit integer, defined as snd_seq_tick_time_t type. The time stored in an event record is a union of these two different time values.
Note that the time format used for real time events is very similar to timeval struct used for unix system time. The absurd resolution of the timestamps allows us to perform very accurate conversions between songposition and real time. Round-off errors can be neglected.
If a timestamp with a relative timestamp is delivered to ALSA, the specified timestamp will be used as an offset to the current time of the queue the event is sent into. An absolute timestamp is on the contrary the time counted from the moment when the queue started.
An client that relies on these relative timestamps is the MIDI input port. As each sequencer queue has it's own clock the only way to deliver events at the right time is by using the relative timestamp format. When the event arrives at the queue it is normalised to absolute format.
The timestamp format is specified in the flag bitfield masked by SND_SEQ_TIME_STAMP_MASK. To schedule the event in a real-time queue or in a tick queue, macros snd_seq_ev_schedule_real() and snd_seq_ev_schedule_tick() are provided, respectively.
If an existing address is set to the destination, the event is simplly delivered to it. When SND_SEQ_ADDRESS_SUBSCRIBERS is set to the destination client id, the event is delivered to all the clients connected to the source port.
A sequencer core has two pre-defined system ports on the system client SND_SEQ_CLIENT_SYSTEM: SND_SEQ_PORT_SYSTEM_TIMER and SND_SEQ_PORT_SYSTEM_ANNOUNCE. The SND_SEQ_PORT_SYSTEM_TIMER is the system timer port, and SND_SEQ_PORT_SYSTEM_ANNOUNCE is the system announce port. In order to control a queue from a client, client should send a queue-control event like start, stop and continue queue, change tempo, etc. to the system timer port. Then the sequencer system handles the queue according to the received event. This port supports subscription. The received timer events are broadcasted to all subscribed clients.
The latter port does not receive messages but supports subscription. When each client or port is attached, detached or modified, an announcement is sent to subscribers from this port.
These functions are the wrapper to the function snd_seq_create_queue(). For releasing the allocated queue, call snd_seq_free_queue() with the obtained queue id.// create a queue and return its id int my_queue(snd_seq_t *handle) { return snd_seq_alloc_named_queue(handle, "my queue"); }
Once when a queue is created, the two queues are associated to that queue record in fact: one is the realtime queue and another is the tick queue. These two queues are bound together to work synchronously. Hence, when you schedule an event, you have to choose which queue type is used as described in the section Time stamp.
On the other hand, in the case of realtime queue, the time resolution is fixed to nanosecononds. There is, however, a parameter to change the speed of this queue, called skew. You can make the queue faster or slower by setting the skew value bigger or smaller. In the API, the skew is defined by two values, the skew base and the skew value. The actual skew is the fraction of them, value/base. As default, the skew base is set to 16bit (0x10000) and the skew value is the identical, so that the queue is processed as well as in the real world.
When the tempo of realtime queue is changed, the tempo of the associated tick queue is changed together, too. That's the reason why two queues are created always. This feature can be used to synchronize the event queue with the external synchronization source like SMPTE. In such a case, the realtime queue is skewed to match with the external source, so that both the realtime timestamp and the MIDI timestamp are synchronized.
For setting these tempo parameters, use snd_seq_queue_tempo_t record. For example, to set the tempo of the queue q
to 48 PPQ, 60 BPM,
void set_tempo(snd_seq_t *handle) { snd_seq_queue_tempo_t *tempo; snd_seq_queue_tempo_alloca(&tempo); snd_seq_queue_tempo_set_tempo(tempo, 1000000); // 60 BPM snd_seq_queue_tempo_set_ppq(tempo, 48); // 48 PPQ snd_seq_set_queue_tempo(handle, tempo); }
For changing the (running) queue's tempo on the fly, you can either set the tempo via #snd_seq_queue_tempo() or send a MIDI tempo event to the system timer port. For example,
There is a helper function to do this easily, snd_seq_change_queue_tempo(). Set NULL to the last argument, if you don't need any special settings.int change_tempo(snd_seq_t *handle, int q, unsigned int tempo) { snd_seq_event_t ev; snd_seq_ev_clear(&ev); ev.dest.client = SND_SEQ_CLIENT_SYSTEM; ev.dest.port = SND_SEQ_PORT_SYSTEM_TIMER; ev.source.client = my_client_id; ev.source.port = my_port_id; ev.queue = SND_SEQ_QUEUE_DIRECT; // no scheduling ev.data.queue.queue = q; // affected queue id ev.data.queue.value = tempo; // new tempo in microsec. return snd_seq_event_output(handle, &ev); }
In the above example, the tempo is changed immediately after the buffer is flushed by snd_seq_drain_output() call. You can schedule the event in a certain queue so that the tempo change happes at the scheduled time, too.
Only calling these functions doesn't deliver the event to the sequencer core but only put to the output buffer. You'll need to call snd_seq_drain_output() eventually.
READ
and WRITE
are permissions of the port from the viewpoint of other ports.
For allowing subscription from/to other clients, another capability flags must be set together with read/write capabilities above. For allowing read and write subscriptions, SND_SEQ_PORT_CAP_SUBS_READ and SND_SEQ_PORT_CAP_SUBS_WRITE are used, respectively. For example, the port with MIDI input device always has SND_SEQ_PORT_CAP_SUBS_READ capability, and the port with MIDI output device always has SND_SEQ_PORT_CAP_SUBS_WRITE capability together with SND_SEQ_PORT_CAP_READ and SND_SEQ_PORT_CAP_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 SND_SEQ_PORT_CAP_WRITE capability, but no SND_SEQ_PORT_CAP_SUBS_WRITE capability is required. Only MIDI input port must have #SND_SEQ_PORT_SUBS_READ capability.
As default, the connection of ports via the third client is always allowed if proper read and write (subscription) capabilities are set both to the source and destination ports. For prohibiting this behavior, set a capability SND_SEQ_PORT_CAP_NO_EXPORT to the port. 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.
To set these values as the connection call like this.snd_seq_addr_t sender, dest; sender.client = MIDI_input_client; sender.port = MIDI_input_port; dest.client = my_client; dest.port = my_port;
snd_seq_port_subscribe_t *subs; snd_seq_port_subscribe_alloca(&subs); snd_seq_port_subscribe_set_sender(subs, &sender); snd_seq_port_subscribe_set_dest(subs, &dest); snd_seq_subscribe_port(handle, subs);
When the connection should be exclusively done only between a certain pair, set exclusive attribute to the subscription record before calling #snd_seq_port_subscribe.
The succeeding subscriptions will be refused.snd_seq_port_subscribe_set_exclusive(subs, 1);
The timestamp can be updated independently on each connection. When set up, the timestamp of incoming queue to the destination port is updated automatically to the time of the specified queue.
For getting the wallclock time (sec/nsec pair), set real attribute:snd_seq_port_subscribe_set_time_update(subs, 1); snd_seq_port_subscribe_set_queue(subs, q);
Otherwise, the timestamp is stored in tick unit. This feature is useful when receiving events from MIDI input device. The event time is automatically set in the event record.snd_seq_port_subscribe_set_time_real(subs, 1);
Note that an outsider client may connect other ports. In this case, however, the subscription may be refused if SND_SEQ_PORT_CAP_NO_EXPORT capability is set in either sender or receiver port.
void capture_keyboard(snd_seq_t *seq) { snd_seq_addr_t sender, dest; snd_seq_port_subscribe_t *subs; sender.client = 64; sender.port = 0; dest.client = 128; dest.port = 0; snd_seq_port_subscribe_alloca(&subs); snd_seq_port_subscribe_set_sender(subs, &sender); snd_seq_port_subscribe_set_dest(subs, &dest); snd_seq_port_subscribe_set_queue(subs, 1); snd_seq_port_subscribe_set_time_update(subs, 1); snd_seq_port_subscribe_set_time_real(subs, 1); snd_seq_subscribe_port(seq, subs); }
This example can be simplified by using snd_seq_connect_to() function.void subscribe_output(snd_seq_t *seq) { snd_seq_addr_t sender, dest; snd_seq_port_subscribe_t *subs; sender.client = 128; sender.port = 0; dest.client = 65; dest.port = 1; snd_seq_port_subscribe_alloca(&subs); snd_seq_port_subscribe_set_sender(subs, &sender); snd_seq_port_subscribe_set_dest(subs, &dest); snd_seq_subscribe_port(seq, subs); }
// ..in the third application (130:0) .. void coupling(snd_seq_t *seq) { snd_seq_addr_t sender, dest; snd_seq_port_subscribe_t *subs; sender.client = 128; sender.port = 0; dest.client = 129; dest.port = 0; snd_seq_port_subscribe_alloca(&subs); snd_seq_port_subscribe_set_sender(subs, &sender); snd_seq_port_subscribe_set_dest(subs, &dest); snd_seq_subscribe_port(seq, subs); }
The subscribed port doesn't have to know the exact sender address. Instead, there is a special address for subscribers, SND_SEQ_ADDRESS_SUBSCRIBERS. 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, use snd_seq_ev_set_subs().
t
(tick) on the queue Q
, the sender must set both schedule queue and time in the event record. The program appears like this: Of course, you can use realtime stamp, too.void schedule_event(snd_seq_t *seq) { snd_seq_event_t ev; snd_seq_ev_clear(&ev); snd_seq_ev_set_source(&ev, my_port); snd_seq_ev_set_subs(&ev); snd_seq_ev_schedule_tick(&ev, Q, 0, t); ... // set event type, data, so on.. snd_seq_event_output(seq, &ev); ... snd_seq_drain_output(seq); // if necessary }
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.void direct_delivery(snd_seq_t *seq) { 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); ... // set event type, data, so on.. snd_seq_event_output(seq, &ev); snd_seq_drain_output(seq); }
void event_filter(snd_seq_t *seq, snd_seq_event_t *ev) { snd_seq_event_t *ev; 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_drain_output(seq); } }