Asynchronous Playback (Howto)

From the ALSA wiki

Jump to: navigation, search


File:Merge-arrows.gif There is another article called HowTo_Asynchronous_Playback with a very similar content. Please be bold an merge the two pages!


WARNING: Asynchronous Playback is part of a not safe subset of ALSA for some Linux distributions that use pulse audio like Ubuntu. Please consider using safer ways on your programs like polling if you are going to use Pulse Audio.

This Howto describes setting up ALSA for playback of sound using the "asynchronous callback" method. With this method data is written to an ALSA output buffer and ALSA is instructed to send an asynchronous notification to the application when it is ready for more data.

In addition to the asynchronous notification technique described in this Howto, an application can use simple polling or memory mapping (mmap).

Contents

About Error Handling

For the sake of clarity, the error checking code is not shown in this Howto; it is left to the programmer to decide what action should be taken if an error is encountered. When necessary, this Howto will present code that is required to releases resources (devices or memory) to recover from an error and restore the system to its original state.

Most functions in alsa-lib API return a negative error code if the operation failed, similar to regular system calls. Also (quite) similar is the 'snd_strerror' function, which turns an error code in a human-readable string, like the strerror function.

The following code presents how the 'snd_strerror' function could be used if, for example, an error occurs while opening the ALSA playback device:

/* Error check */
if (err < 0) {
  fprintf (stderr, "cannot open audio device %s (%s)\n", 
    device_name, snd_strerror (err));
  pcm_handle = NULL;
  return;
  }

Opening the device

Before using an ALSA device, it is necessary to open it first. To do this, the 'snd_pcm_open' API function is used.

Data Types

/* This holds the error code returned */
int err; 

/* Our device handle */
snd_pcm_t *pcm_handle = NULL;

/* The device name */
const char *device_name = "default"; 

The 'err' variable will hold the return value of the function.

Function Call

/* Open the device */
err = snd_pcm_open (&pcm_handle, device_name, SND_PCM_STREAM_PLAYBACK, 0);

This should look fairly straight-forward. We need


Question for Developers:Can this be OR'd? And what exactly is the SND_PCM_ASYNC flag for? The asynchronous notification worked without this flag, in fact, it broke when I tried to set it.


Closing the device

Normally the ALSA device would not be closed until the application has finished using it but for the sake of clarity of this Howto, it is presented here.

snd_pcm_close (pcm_handle);

Hardware parameters

Structures

Before data can be sent to the newly opened device, that data needs to be initialized. All of the required data is contained in one structure of the type 'snd_pcm_hw_params_t'.

 snd_pcm_hw_params_t *hw_params;

A variable of this type is dynamically allocated using the 'snd_pcm_hw_params_malloc' API function.

 snd_pcm_hw_params_malloc (&hw_params);

The hardware parameters structure needs to be initialised with the device capabilities.

 snd_pcm_hw_params_any(pcm_handle, hwparams);

Hardware parameters are specified, queried, and modified using a set of API function which follow the naming convention, "snd_pcm_hw_params_OPERATION" where "OPERATION" defines that which is to be performed.

Some of the hardware parameter API functions modify the parameter passed to it (in which case, the parameter's address is passed). Since constants can not be used in such situations (even if it the "returned" value is ignored), it is necessary to define variables for these parameters.

For example, the API function 'snd_pcm_hw_params_set_rate_near' modifies the frame rate variable that was passed; therefore it is necessary to declare such a variable:

unsigned int rrate = 44100;

Mandatory Parameters

Not all hardware parameters need to be specified, in many cases the defaults provided by the ALSA system will serve without modification. Nonetheless, setting a subset of hardware parameters is almost mandatory.

Access Mode

The 'snd_pcm_hw_params_set_access' function is used to set the transfer mode.

snd_pcm_hw_params_set_access (pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);

There are two types of transfer modes:

Besides this, there are also two ways to represent the data transfered, interleaved and non-interleaved. If the stream you're playing is mono, this won't make a difference. In all other cases, interleaved means the data is transfered in individual frames, where each frame is composed of a single sample from each channel. Non-interleaved means data is transfered in periods, where each period is composed of a chunk of samples from each channel.

Sound Stream Example  :

To visualize the two different data representations, consider a 16-bit stereo sound stream.

Interleaved would look like :

LL RR LL RR LL RR LL RR LL RR LL RR LL RR LL RR LL RR LL RR ...

Non-interleaved might look like :

LL LL LL LL LL RR RR RR RR RR LL LL LL LL LL RR RR RR RR RR ...

where each character represents a byte in the buffer, and padding should of course be ignored (it's just for clarity).


snd_pcm_hw_params_set_format (pcm_handle, hw_params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_rate_near (pcm_handle, hw_params, &rrate, NULL);
snd_pcm_hw_params_set_channels (pcm_handle, hw_params, 2);

\

So now that we've got two transfer modes and two ways to organize our sound stream, we can conclude there's 4 options we can pass to snd_pcm_hw_params_set_access:


Question for Developers:There's also a SND_PCM_ACCESS_MMAP_COMPLEX, which is described in the API reference as "mmap access with complex placement". Anyone care to explain how that works?


Sample Format

So now that we've set our access mode, we specify the sample format. There's a whole range of sample formats ALSA supports. You can find a list on the ALSA Document Library website.

The sample format we picked above is Signed 16-bit Little-Endian samples. A pretty common sample format, and it should be obvious how these parameters are named.

Sample Rate

After that, we set the sample rate of our stream, in Hz. Note that the functions we use is called snd_pcm_hw_params_set_rate_near, meaning the actual sample rate set may not match the sample rate we specify. For this reason the function takes a pointer to an unsigned integer, so it can change the value of our rrate variable to reflect the actual rate set. There's also a function snd_pcm_hw_params_set_rate, which doesn't take a pointer, and will try to set the sample rate to the exact rate you specify. It's likely that this will fail on different sound devices though.


Note:The last parameter to snd_pcm_hw_params_set_rate_near specifies the "Sub unit direction". It should be set to "-1" to specify that the actual rate should be less than the supplied parameter, "1" for an actual rate greater than the rate supplied, and "0" for exactly the supplied rate.


Number of Channels

How we tell our device how many channels our stream contains should be obvious now from the code above.

Two other hardware parameters which might be helpful to mention are buffer size and period size:

/* These values are pretty small, might be useful in
  situations where latency is a dirty word. */
snd_pcm_uframes_t buffer_size = 1024;
snd_pcm_uframes_t period_size = 64;

snd_pcm_hw_params_set_buffer_size_near (pcm_handle, hw_params, &buffer_size);
snd_pcm_hw_params_set_period_size_near (pcm_handle, hw_params, &period_size, NULL);

Both of these take the size in bytes. The period size is what I described earlier in the section about interleaved and non-interleaved. Besides specifying the size in bytes you can also specify them in nanoseconds using the hardware parameters buffer_time and period_time.

There's a whole lot of other hardware variables you can alter, but that'd be too much information to cover here. Take a look at the API reference if you're interested.

Setting the Hardware Parameters

Now that we're done setting all our hardware parameters, we have to apply them back to the device. This is easy, really:

snd_pcm_hw_params (pcm_handle, hw_params);

\

Wrapping Up

Of course, we also have to clean up the structure behind our back. Simply free it up like this:

snd_pcm_hw_params_free (hw_params);

Software Parameters

Structures

Besides hardware parameters, there are also software parameters. There's alot less of 'em, but in our case, we have to deal with them. Setting them is almost exactly the same as hardware parameters, except with, you guessed it, an snd_pcm_sw_params_t structure instead:

snd_pcm_sw_params_t *sw_params;

snd_pcm_sw_params_malloc (&sw_params);
snd_pcm_sw_params_current (pcm_handle, sw_params);

Timing Parameters

Software parameters are optional in most cases, but in this case we're going to use asynchronous notification to let ALSA tell us when we need to fill the buffer with some new data. And in this case, software parameters are handy, because they allow us to set a threshold for when that happens and also when ALSA starts actually playing something:

snd_pcm_sw_params_set_start_threshold(pcm_handle, sw_params, buffer_size - period_size);
snd_pcm_sw_params_set_avail_min(pcm_handle, sw_params, period_size);

The function 'set_start_threshold' tells ALSA when to start playing. In this case, we tell it to wait until our buffer is almost full.

The function 'set_avail_min' tells ALSA when to notify us. In this case, we want to be able to write atleast period_size samples to the buffer without blocking.

Just like hardware parameters, other available software parameters are covered in the API Reference.

Setting the Software Parameters

And just like hardware parameters, we have to apply the changes we made back to the device using this simple piece of code:

snd_pcm_sw_params(pcm_handle, sw_params)

Wrapping up

And because we don't want to cause any leaks, atleast not in memory, we clean up:

snd_pcm_sw_params_free (sw_params);

Writing to the Device

Preparation

Now we've set up our device, but we still can't write to it. There's one last step we have to take: prepare the device:

snd_pcm_prepare (pcm_handle);

Writing the First Chunk

Now we can write! Before we start playback it's a good idea to write an initial chunk of sound to the device:

snd_pcm_writei (pcm_handle, MyBuffer, 2 * period_size);

First of all, we have been using the interleaved format throughout this example as you've seen above. If you were using the non-interleaved format, you'd use the snd_pcm_writen function instead. Other than that, it works exactly the same.

"MyBuffer" in the above piece of code is a pointer to wherever the data is you want to write to the device. and the last parameter is the size in bytes of the data to write.


Note:Two times the period size seems to be a good initial write size? Going below this results in a buffer underrun (broken pipe error on any subsequent function calls) and going over it seems to trigger "file descriptor in a bad state" errors.



Note:The write functions actually return something useful beside error codes as well; i.e., if the write succeeds, the length of the data actually written to the device is returned. You probably want to check that this is the same size as the data you intended to send.


The Callback Notification

Initializing the Callback

So now it's time to actually get asynchronous! The way ALSA notifies us, is through a callback. We tell it which callback simply by calling a function:

snd_async_handler_t *pcm_callback;

snd_async_add_pcm_handler(&pcm_callback, pcm_handle, MyCallback, NULL)

The first parameter to this function is simply a pointer to a handle. This handle is also a parameter to our callback, as I will show you later on. The second parameter is our device handle again, the third our callback function and the fourth is a void pointer to any user data you may want to pass to the callback. I'll just leave it set to NULL in this example.

Cancelling the Callback

You can disable the callback at any time by calling the snd_async_del_handler function, like this:

snd_async_del_handler (pcm_callback);

However, it is not necessary to clean up the callback. It will be cleaned up when the device is closed anyways.

Starting (Enabling) the Callbacks

So when we've been through all this trouble to set up our device, we want to actually start playing. This is another easy part:

snd_pcm_start(pcm_handle);

Now you can do pretty much anything else in your application. If there's not much else to do, there's always the sleep system call.

Stopping the Callback

At any time, you can stop playback by calling any of these two functions:

snd_pcm_drop(pcm_handle);

Which immediatly stops playback, dropping any frames still left in the buffer, or:

snd_pcm_drain(pcm_handle);

Which 'drains' the buffer, stopping after all remaining frames in the buffer have finished playing.

For other operations related to playback or the device structure, again, see the API reference

Processing the Callback

Filling the Buffer

To prevent buffer underruns, we need to refill the buffer every so often, adding new stream data to it to keep the sound playing smoothly. This is where our callback comes in.

The callback has the following signature:

void MyCallback(snd_async_handler_t *pcm_callback);

What happens in the callback is fairly simple; this is simply and roughly what it will look like:

void MyCallback(snd_async_handler_t *pcm_callback) {
                snd_pcm_t *pcm_handle = snd_async_handler_get_pcm(pcm_callback);
  snd_pcm_sframes_t avail;
  int err;

  avail = snd_pcm_avail_update(pcm_handle);
  while (avail >= period_size) {
     snd_pcm_writei(pcm_handle, MyBuffer, period_size);
     avail = snd_pcm_avail_update(handle);
     }
 }

As you can see, the snd_async_handler_t structure is passed to the callback. This is a really handy structure, because the first thing we do is grab the associated device handle from it. If you had any user data associated with it, you can get to it like this:

void *private_data = snd_async_handler_get_callback_private(pcm_callback);

Where 'void *' would, of course, be whatever type the original data you passed to the snd_async_add_pcm_handler function was. In our case, however, this would simply return NULL.

Another new function we see here is snd_pcm_avail_update. This function takes a device handle, and returns the amount of available bytes that can be written to the device. As you can see, we use this in our loop too keep writing chunks to the buffer until it's full.

That is basically all there is to the callback.


Note:This example does not include error checking, which is a good idea, of course. Occasionally, you may have to restore from a buffer underrun, in case the computer was caught up in some other process and didn't get to calling your callback. Whenever this happens, the first operation you perform on the device, in this case snd_pcm_avail_update, will return -EPIPE (a 'Broken Pipe' error). You can restore from this by calling the snd_pcm_prepare function again.


Retrieved from "http://alsa.opensrc.org/Asynchronous_Playback_(Howto)"

Category: Howto

GITHUB | EDIT