Written by J. Laroche at the Center for Music Experiment at UCSD, San Diego California. December 1990.

The Sound/DSP Driver.

The sound driver provides an efficient way of making connections between several devices, passing data through these connections, and getting messages to keep track of what's happening. It also provides a simple way of talking to the DSP.

The connections are called streams. You can create streams going from the memory to the DACs (to play a sound already in the memory), from the microphone to the memory (to record a sound from the CODEC microphone), from the memory to the DSP chip, or through it to the DACs (to transform and play a sound already in the memory) etc... You can create several streams at the same time (for example, one from the memory to the DSP and one from the DSP to the memory, to process data and get them back in the memory).

The sound/dsp driver functions are documented, but it's a good idea to keep a copy of the include files /usr/include/sound/sounddriver.h and /usr/include/sound/snddriver_client.h at hand reach for a concise summary of the functions arguments, and a description of the few structures used.

A simple example: playing back a sound using the sounddriver functions.

To see how this works, here's a very simple example of a program reading a sound-file, setting-up a stream from the memory to the DACs, playing the sound-file and controling its play-back. It does not involve the DSP.
This code can be found in Examples/01_Play_Dacs/perso_b.c

// ----------------------- Beginning of program

#import <sound/sound.h>
#import <sound/sounddriver.h>
#import <mach.h>
#import <stdio.h>

#define Error(A,B) if((A)) {fprintf(stderr,"%s %s\n",B, SNDSoundError((A)));\
mach_error(B,(A)); }

static void read_completed(void *arg, int tag)
{
fprintf(stderr,"playing completed message called \n");
}

static void read_started(void *arg, int tag)
{
fprintf(stderr,"Playing Started message received \n");
}

static void read_aborted(void *arg, int tag)
{
fprintf(stderr,"Playing Aborted message received \n");
}

static void read_paused(void *arg, int tag)
{
fprintf(stderr,"Playing Paused message received \n");
}

static void read_resumed(void *arg, int tag)
{
fprintf(stderr,"Playing Resumed message received \n");
}

main (int argc, char *argv[])
{
    int i;
    static port_t dev_port, owner_port;
    static port_t reply_port, write_port;
    kern_return_t k_err;
    snddriver_handlers_t handlers = { 0, 0, 
    read_started,read_completed,read_aborted,read_paused,read_resumed, 0, 0};
    msg_header_t *reply_msg;
    SNDSoundStruct *sound;
    short *location;
    int low_water = 48*1024;		// Used by the driver to control the flow of samples
    int high_water = 64*1024;		// Used by the driver to control the flow of samples
    int DMASIZE = 4096;
    int WRITE_TAG = 1;
    int length,protocol;

    if(argc == 1) { printf("I need a 16bit linear sound file...\n");
    exit(1);}

    k_err = SNDAcquire(SND_ACCESS_OUT,0,0,0,
    		NULL_NEGOTIATION_FUN,0,&dev_port,&owner_port); 
    Error(k_err,"SND acquisition  ");
    
    k_err = port_allocate(task_self(),&reply_port);

    k_err = SNDReadSoundfile(argv[1], &sound);
    Error(k_err,argv[1]);

    k_err = SNDGetDataPointer(sound,(char**)&location,&length,&i);
    Error(k_err,"Data Pointer");

    k_err = snddriver_stream_setup(dev_port, owner_port,
			SNDDRIVER_STREAM_TO_SNDOUT_44,
			DMASIZE, 2, 
			low_water, high_water,
			&protocol, &write_port);
    Error(k_err,"Stream  ");
    
    reply_msg = (msg_header_t *)malloc(MSG_SIZE_MAX);
    while (1) 
	{
	int i;
	
	printf("1: receive message  2: write  3: abort 4:pause 5:resume\n");
	scanf("%d",&i);
	    switch(i)
	    {
		case 1 : 
		printf("Asking for messages ... \n");
		reply_msg->msg_size = MSG_SIZE_MAX;
		reply_msg->msg_local_port = reply_port;
		k_err = msg_receive(reply_msg, RCV_TIMEOUT, 500);
		Error(k_err,"Message  ");
		if(!k_err) snddriver_reply_handler(reply_msg,&handlers);
		break;
		
		case 2 :
		k_err = snddriver_stream_start_writing(write_port,location,
			length,WRITE_TAG,0,0,1,1,1,1,1,0, reply_port);
		Error(k_err,"Write Command  "); break ;
		
		case 3 : 
		snddriver_stream_control(write_port,1,SNDDRIVER_ABORT_STREAM);
		break ;
		
		case 4 : 
		snddriver_stream_control(write_port,2,SNDDRIVER_PAUSE_STREAM);
		break ;
		
		case 5 : 
		snddriver_stream_control(write_port,2,SNDDRIVER_RESUME_STREAM);
		break ;
	    }
	}
}

//-------------- End of program

To compile this program, you would use
	cc -o myProgram -Wall myProgram.c -lsys_s

INCLUDES, DEFINES and VARIABLES.

The include files are:
#import <sound/sound.h>		for the various sound utilities functions.
#import <sound/sounddriver.h>	for the sound/dsp driver functions.
#import <mach.h>		for messages and ports.
#import <stdio.h>		for stderr (in fprintf(stderr...))

The macro 
#define Error(A,B) if((A)) {fprintf(stderr,"%s %s\n",B, SNDSoundError((A)));\
mach_error(B,(A)); }

gives you more info about the error code returned by the C functions we use. Some of them return errors recognized by SNDSoundError(), others by mach_error(). To be sure to get as much info as possible, we invoque the two functions for any error code, hoping that one at least will identify the error.

The five following defined functions are used to keep track of what's happening. We'll come back to it later.
Among the variables declared at the beginning of the main program, you'll find:

    static port_t dev_port, owner_port, reply_port, write_port;

These are MACH ports (see the mach operating system chapter in the doc) used to send and receive messages. They don't necessary need to be declared as static, but always make sure they are initialized to zero (we'll come back to that later.)

    SNDSoundStruct *sound;

sound is a pointer to a SNDSoundStruct structure which will contain the sound we're going to play. This structure is described in the documentation in the "sound" chapter. It contains a header showing the size, format and other info about the sound.

INITIALIZING THE DRIVER:

The first thing you need to do is to acquire the sound out device (the DACs.) This is done by the function

    k_err = SNDAcquire(SND_ACCESS_OUT,0,0,0,
    		NULL_NEGOTIATION_FUN,0,&dev_port,&owner_port);
		 
See the description of this function in the documentation's Sound Functions chapter. When this function returns successfully, dev_port represents the sound device port, and owner_port, attached to the current task (see mach OS docu) represents the current owner of the device (our program). We'll need these two ports for further calls to the sound driver.
WARNING! dev_port and owner_port have to be initialized to zero, otherwise the function will be unable to acquire the sound device! This is tricky. It is a good precaution to declare dev_port and owner_port as static, just to make sure they are properly initialized.

    k_err = SNDReadSoundfile(argv[1], &sound);
    k_err = SNDGetDataPointer(sound,(char**)&location,&length,&i);

These two functions are sound utilities to read a sound file, find where the samples are located, and retrieve the length of the sound.

    k_err = snddriver_stream_setup(dev_port, owner_port,
			SNDDRIVER_STREAM_TO_SNDOUT_44,
			DMASIZE, 2, 
			low_water, high_water,
			&protocol, &write_port);

This is the heart of the matter. This snddriver function create a new stream through which we'll be able to pass samples. Dev_port and owner_port have been retrieved by SNDAcquire(), SNDDRIVER_STREAM_TO_SNDOUT_44 indicates that the stream goes from the memory to the DACs with a sampling rate of 44100 Hz. DMASIZE indicates the size of each DMA package, and must be a power of 2 greater than 16. Usual values are 1024, 2048 and 4096. In the current case, any will do (DMA is explained in 05_Dig_Ears.) The parameter 2 indicates that we're going to send short int (2 bytes) to this stream. low_water and high_water are used by the driver to optimize its functioning. You can use higher values for high_water, to make the driver works more safely. protocol is an int which you would use if you used the DSP. Here, we'll just ignore it.
Finally, write_port represents the new stream. Whenever we'll want to take an action on that stream, we'll identify it by its port, write_port.

PLAYING THE SOUND AND CONTROLLING THE PLAY-BACK

    k_err = snddriver_stream_start_writing(write_port,location,
			length,WRITE_TAG,0,0,1,1,1,1,1,0, reply_port);

enqueues a write command on the write_port. It means that we're going to start writing samples to the write_port. Since this port represents a stream going to the DACs, this is going to play-back the sound. Location is the address of the first sample, length is the number of samples to play, WRITE_TAG is a number which identifies the region (data) we just enqueued. We could enqueue different regions on the same stream, and assign a different tag to each region, then be able to control them separately. The rest concerns messages, we'll get back to it later. At this point, the stream starts playing the sound through the DACs at a sampling rate of 44100 Hz. 

To control the play back, we use

	snddriver_stream_control(write_port,TAG,CONTROL_OPTION);

This makes it possible to pause, resume or abort the play-back. The write_port identifies the stream you want the control to apply to, and the TAG identifies which region this control is to be applied to. CONTROL_OPTION may be SNDDRIVER_PAUSE_STREAM to pause the stream, SNDDRIVER_RESUME_STREAM to resume playing back and so on.
WARNING! If the tag of the region you enqueued is not the same as the tag you use for the control, the control DOES NOT apply to the region, unless TAG is zero in which case it applies to all regions. BUT because of a bug in the driver (fixed?) this is not true for pausing and resuming. Pausing and resuming apply to all regions regardless of their tag.

GETTING MESSAGES: WHAT'S GOING ON?

The driver implements a nice way of keeping track of what's going on. If you allocated a reply_port, and passed it to the snddriver_stream_start_writing() function, the driver can send messages to the reply_port when the stream starts playing, pauses, resumes, aborts, etc... These messages can then be retrieved on that port using a mach function, then dispatched using a message handler.

Messages are used by the MACH operating system to communicate between ports. A message is a C structure composed of a fixed header followed by a variable number of data items. It is merely an array of bytes whose number can range from just the size of the header to a maximum fixed by the operating system. The driver uses messages to pass data or pointers to data, etc...
Read the MACH O.S. chapter for more info about messages.

The message passing mechanism in the case of the sound driver is done in three parts:
- create a new port where messages will be sent, and ask the driver to send them in precise cases.
- retrieve the messages on the receiving port.
- use the message to trigger actions according to its nature.

I Setting-up the message sending mechanism: 

    k_err = port_allocate(task_self(),&reply_port);

allocate a new port, and attach it to the current task (our program). When we start playing the sound, we will pass this port to the sound driver to tell it where to send its messages:

    k_err = snddriver_stream_start_writing(write_port,location,
			length, WRITE_TAG,0,0,1,1,1,1,1,0, reply_port);

The last argument of this function is the reply_port: all messages concerning this stream will be sent to the reply_port. The set of values just before the reply port indicates which messages we want to get (see documentation). 
For example, if we want to get a message when the region starts writing (the sound starts playing), we'll put a 1 in the 7th argument of the snddriver_stream_start_writing() function. The driver will send a "write started" message to the reply_port when the region starts writing. 

II Retrieving the messages arriving to the reply port:

You need to declare a pointer to a message header and allocate memory where the received message will be copied.
 
    msg_header_t *reply_msg;
    reply_msg = (msg_header_t *)malloc(MSG_SIZE_MAX);
   
MSG_SIZE_MAX is a define which contains the maximum size of any message. This way, you can be sure your *reply_msg is big enough. Now, reply_msg points to a area in the memory where the received message can be copied.

To retrieve the message received at the reply_port, we'll use the msg_receive() mach function, and we'll pass it the pointer to the memory we just allocated to tell it where to copy the received message. But before that, we need to initialize two fields of the header: its size and the port where the message is to be retrieved.

    reply_msg->msg_size = MSG_SIZE_MAX;
    reply_msg->msg_local_port = reply_port;
    
Now we can call the mach function

    k_err = msg_receive(reply_msg, RCV_TIMEOUT, 500);

It will look at reply_msg->msg_local_port to find where to expect a message, then it will retrieve the first message received on that port, or wait if none has been received yet, depending on the option selected in the second argument (see mach functions documentation). The retrieved message is then copied in the buffer pointed to by reply_msg. In our case, the function waits 500 ms for a message to arrive and returns "RCV_TIMED_OUT" if none arrived during that time.

WARNING: We can used the same buffer reply_msg for several messages BUT: when the received message is copied into the buffer we allocated, its field reply_msg->msg_size is overwritten and now contains the actual size of the message. To receive a new message, you'll need to reset it to MSG_SIZE_MAX. If you don't, you might get a "RCV_TOO_LARGE" error (depending on what the previous message was and what the next is.)

At this point, we have a message at the address reply_msg. We still don't know what it contains, and how to use it.

III Handling the received message:

You could handle the message by looking at its various fields. This would be very painful. Fortunately, the sound driver provides a much simpler way of doing that:
given a message handler

    snddriver_handlers_t handlers = { 0, 0, 
    read_started,read_completed,read_aborted,read_paused,read_resumed, 0, 0};

the function

    snddriver_reply_handler(reply_msg,&handlers);

helps you dispatch the message to the appropriate functions:
The handler is a C structure composed of two int followed by an array of pointers to functions that the user provides. snddriver_reply_handler() will find out what kind of message reply_msg points to (started, paused, resumed, etc...) and call the corresponding function provided in the handler structure.
For example, if we asked for a "completed" message in our call to snddriver_stream_start_writing() by setting the 8th argument to 1 and providing a reply port, the sound driver will send a "completed" message to the reply port when playing is completed. Then, msg_receive() will retrieve this message, and snddriver_reply_handler() will recognize a "completed" message, and call the function pointed to by the 4th argument: read_completed().

This scheme is a bit confusing at first, but turns out to be very efficient. To make things easier, you should keep a paper copy of the include file /usr/include/sound/sounddriver.h where all the info concerning handlers and user-supplied functions is concentrated.

The example presented above is only intended to demonstrate the use of sound/dsp driver functions. If you need to play-back sound files, a much more efficient way of doing it would be to use the SNDStartPlaying() function. The sound/dsp driver is useful for more complex applications.

PROBLEM SHOOTING:

· If SNDAcquire() returns "Cannot access hardware resources", check that the sound device is not currently busy (you can use the unix command dspbeep which generates a sound on the dsp and plays it.). The second possibility is that the dev_port and/or owner_port you passed were not initialized to zero. In that case (and it seems to be a bug) the function doesn't work properly.

· If snddriver_stream_setup() returns "Bad size", check that the DMASIZE you passed is correct (that is, a power of 2 great than or equal to 16), and that the sample size is 2 (sizeof(short)).

· If the stream controls have no effect, check that the tag you used in the snddriver_stream_start_writing() function is the same as the one in snddriver_stream_control(). 

· If you don't get any message from the driver, check that:
- You allocated a reply_port.
- You asked for the correct messages in your snddriver_stream_start_writing() function, and passed the correct reply_port.
- You allocated a message header and set its local port to reply_port and its size to MSG_SIZE_MAX.
- msg_receive() returns a zero value (indicating that a message has been retrieved.)
- You provided a function in your reply_handler corresponding to the kind of message you asked.

If you still don't get any message, take a walk or go play pool. Tomorrow is another day.