[Previous] [Contents] [Next]

Playing Audio Data

Throughout this chapter you'll see examples that contain code fragments that come from a functional, text-mode .wav file player. The complete file is in the wave.c example in the appendix. You may wish to compile and run the application now and then reference the running code as you progress through this chapter.


Note:

Under construction

New QNX RTP APIs are being developed for future releases. When they become available, we'll update the documentation.


Opening your audio device

The first thing you need to do in order to play audio is to open a connection to the audio device. This connection is represented by an audio handle that you must pass into every API call. But, before you can open the device you must select it.

Selecting Card and Device values

At any given time you may have more than one audio card active in your system, and each card may have more than one PCM device. You can see what devices are currently active on your system by looking in the /dev/snd directory. For example, the Trident 4D-Wave driver includes the following devices:

controlC0
Control interface for Card 0
mixerC0D0
Mixer interface for Card 0 Device 0
pcmC0D0c
Audio Channel Card 0 Device 0 for CAPTURE
pcmC0D0p
Audio Channel Card 0 Device 0 (front) for PLAYBACK
pcmC0D1p
Audio Channel Card 0 Device 1 (rear) for PLAYBACK
pcmC0D2p
Audio Channel Card 0 Device 2 (SPDIF) for PLAYBACK

The specific card value assigned to a piece of hardware is determined at runtime. It's dependent on the order in which the drivers are started. Each card is assigned the lowest card number available at the time that it registers itself. The device values are driver specific and generally correspond to hardware ports. There are three techniques that your application can use to determine its card and device numbers: explicit selection, automatic selection, and using the mpsettings configuration file.

Explicit Selection

If you've intimate knowledge and total control over your system configuration, you can explicitly hardcode the card and device numbers directly into your application. This approach is generally not recommended because it's the most inflexible of the three.

Automatic Selection

The API includes two calls that can be used to find suitable card and device pairs for you;

snd_pcm_open_preferred()
Takes as input a pointer to card and device, as well as the audio handle. It opens the preferred card and device pair and returns their values. You don't need to call snd_pcm_open() as it returns a valid open audio handle.
snd_pcm_find()
Query the hardware to find which card/device pairs support a given playback format (FMT). You call this routine with an array of card/devices. Pass in: a format value (can be ORed together), the depth of the declared arrays, and pointers to the card and device arrays. This call returns the arrays filled with the valid card/device pairs and the field used to tell the depth of the declared arrays is set to the number of card/device pairs returned.

Using mpsettings

This option is Photon-specific. The Neutrino Media Player, phplay uses calls into libmedia.so to read and write the file $(HOME)/.ph/mpsettings. This is the persistent store of the phplay configuration settings.

Three calls defined in <photon/MvReg.h> are used to fill and export the MvSetup_t structure. The mpsettings file is a way to override what would normally come out of a call to snd_pcm_open_preferred().


Note: When phplay users press the Defaults button to restore the mpsettings file to a default state, libmedia.so calls snd_pcm_open_preferred() and snd_pcm_find().


Note: More information on the mpsettings file is available in the Multimedia Technote for the QNX RTP.

Opening the Audio device

As you can see from the following example, wave.c has two possible open calls. If the card and device numbers are specified on the command line by the user, the device is opened explicitly using snd_pcm_open(). Otherwise, the default playback device is opened by snd_pcm_open_preferred() and the card and device variables are implicitly set to appropriate values. In either case, the pcm_handle now represents a valid connection to the audio hardware.

if (card == -1)
{
    if ((rtn = snd_pcm_open_preferred (&pcm_handle, &card, &dev, SND_PCM_OPEN_PLAYBACK)) < 0)
        return err ("device open");
}
else
{
    if ((rtn = snd_pcm_open (&pcm_handle, card, dev, SND_PCM_OPEN_PLAYBACK)) < 0)
        return err ("device open");
}

Configuring the audio channel

Now you have a valid handle for your audio device. However, before you can use it, you need to configure it.

Interfaces

There are two main interfaces to the asound library: regular or plugin.

regular interface
Does minimal processing on your request. It simply forwards it to the driver. (e.g. snd_pcm_channel_params()).
plugin interface
Performs tasks that make your code more portable between audio devices. (e.g. snd_pcm_plugin_params()) This interface converts sample rates, sample widths, number of voices, etc., to circumvent the limitations of the actual hardware. For example, if you need a 48 kHz channel, but your hardware only supports 44.1 kHz, the plugin interface shifts the sample rate for you (within the libasound.so library) and makes the application think it has a 48 kHz device.

Note: Don't mix and match the interfaces. If you need to use the plugin interface, be consistent and only use plugin calls, if one is available.

Modes

There are two modes that you can use to read and write to and from the drivers: block mode and stream mode. You set the mode in the snd_pcm_channel_params structure that's passed as an argument to params.mode. Valid choices are: SND_PCM_MODE_BLOCK and SND_PCM_MODE_STREAM.

block mode
Treats the audio data as a series of discrete fragments of a particular size. According to the original ALSA 5 specification, only writes of an integral number of fragments (or blocks) succeed, any other size fails. This isn't the case with the current Neutrino drivers if you use the plugin interface. When you write a partial fragment into the plugin interface, the asound library automatically buffers it for you until it's collected a full fragment or a flush is issued. With the standard read and write calls you must write multiples of the fragment size.
stream mode
Treats the audio data as a continuous queue of data. Writes of any size are accepted, unless the buffer is full.

Specifying parameters

The first call in the following example is to snd_pcm_plugin_set_disable(). It's used to disable the Mmap interface. This is optional. By default, the plugin interface invokes the Mmap interface on your behalf (block mode only). A status call through the Mmap interface doesn't return count (the number of bytes in the hardware buffer) or scount (the number of bytes processed, or played, since last underrun). As described in the Audio Synchronization Mechanisms chapter, these values can be useful for synchronization (e.g. lip sync) of different media. If you require synchronization, you'll want to disable the Mmap interface.

The following example shows how wave.c configures the device to use the maximum fragment size supported by the device, and the smallest number of fragments (which is 2). It also configures the device for playback of interleaved samples at the rate, format, and voices (mono or stereo) specified by the .wav file.

if ((rtn = snd_pcm_plugin_set_disable (pcm_handle, PLUGIN_DISABLE_MMAP)) < 0)
{
    fprintf (stderr, "snd_pcm_plugin_set_disable failed: %s\n", snd_strerror (rtn));
    return -1;
}

memset (&pi, 0, sizeof (pi));
pi.channel = SND_PCM_CHANNEL_PLAYBACK;
if ((rtn = snd_pcm_plugin_info (pcm_handle, &pi)) < 0)
{
    fprintf (stderr, "snd_pcm_plugin_info failed: %s\n", snd_strerror (rtn));
    return -1;
}

bzero (&pp, sizeof (pp));

pp.mode = SND_PCM_MODE_BLOCK;
pp.channel = SND_PCM_CHANNEL_PLAYBACK;
pp.start_mode = SND_PCM_START_FULL;
pp.stop_mode = SND_PCM_STOP_STOP;

pp.buf.block.frag_size = pi.max_fragment_size;
pp.buf.block.frags_max = 1;
pp.buf.block.frags_min = 1;

pp.format.interleave = 1;
pp.format.rate = mSampleRate;
pp.format.voices = mSampleChannels;

if (mSampleBits == 8)
    pp.format.format = SND_PCM_SFMT_U8;
else
    pp.format.format = SND_PCM_SFMT_S16_LE;

if ((rtn = snd_pcm_plugin_params (pcm_handle, &pp)) < 0)
{
    fprintf (stderr, "snd_pcm_plugin_params failed: %s\n", snd_strerror (rtn));
    return -1;
}

Confirming your setup

After configuring the device, you can read back the current settings as shown in the following example. This is optional, but recommended. If it's invalid, the frag_size parameter (unlike the other parameters) may be changed by the driver to a valid setting. So, the only way that you can be certain of the size and number of your buffer fragments is to verify them.

memset (&setup, 0, sizeof (setup));
setup.channel = SND_PCM_CHANNEL_PLAYBACK;
setup.mixer_gid = &group.gid;
if ((rtn = snd_pcm_plugin_setup (pcm_handle, &setup)) < 0)
{
    fprintf (stderr, "snd_pcm_plugin_setup failed: %s\n", snd_strerror (rtn));
    return -1;
}
printf ("Format %s \n", snd_pcm_get_format_name (setup.format.format));
printf ("Frag Size %d \n", setup.buf.block.frag_size);
printf ("Rate %d \n", setup.format.rate);
bsize = setup.buf.block.frag_size;

Attaching to the mixer

When you confirm your setup, you can optionally specify a pointer to a mixer_gid (as shown in the previous example) to request information on the mixer group associated with this PCM subdevice. For more information on using the mixer, see the Controlling volume and balance chapter.

Handling writes

Preparing for writes

The channel must be in the SND_PCM_STATUS_PREPARED or SND_PCM_STATUS_RUNNING state for it to accept writes (or reads) from the application. This is a very simple but necessary call that must happen after the audio device is configured, but before the first write occurs.

if ((rtn = snd_pcm_plugin_prepare (pcm_handle, SND_PCM_CHANNEL_PLAYBACK)) < 0)
    fprintf (stderr, "snd_pcm_plugin_prepare failed: %s\n", snd_strerror (rtn));

Writes to the audio subsystem can be blocking or nonblocking. They must be handled differently, and are suited for different designs. In general, we recommend using blocking calls because they're simpler to use. Nonblocking writes are only advantageous in single-threaded applications that have multiple data sinks and/or sources that must be monitored (e.g. a single-threaded Photon app). Unless you've a lot of existing audio code that requires single-threaded execution, it's much simpler and easier to spin-off a worker thread for audio and use the blocking calls. Both blocking and nonblocking writes return the number of bytes accepted by the driver or a negative error code.

Blocking writes
A blocking write returns only after all data is accepted by the driver or when an error occurs. Therefore, if the return value isn't equal to the amount sent, you'll have to verify the channel. To do this you read status. If status is SND_PCM_STATUS_READY or SND_PCM_STATUS_UNDERRUN (SND_PCM_STATUS_OVERRUN for capture channels) then you'll have to reprepare the channel. After you've reprepared it, you can attempt to write out the rest of the data.

The following example verifies that the number of bytes written matches the number of bytes requested for the write. If the write returns a value less then the requested, an error occurs and is handled as follows:

snd_pcm_channel_status_t status;
int written = 0;

if ((n = fread (mSampleBfr1, 1, min (mSamples - N, bsize), file1)) <= 0)
    continue;
written = snd_pcm_plugin_write (pcm_handle, mSampleBfr1, n);
if (written < n)
{
    memset(&status, 0, sizeof(status));
    if (snd_pcm_plugin_status (pcm_handle, &status) < 0)
    {
        fprintf (stderr, "underrun: playback channel status error\n");
        exit (1);
    }
    if (status.status == SND_PCM_STATUS_READY ||
        status.status == SND_PCM_STATUS_UNDERRUN)
    {
        if (snd_pcm_plugin_prepare (pcm_handle, SND_PCM_CHANNEL_PLAYBACK) < 0)
        {
            fprintf (stderr, "underrun: playback channel prepare error\n");
            exit (1);
        }
    }
    if (written < 0)
        written = 0;
    written += snd_pcm_plugin_write (pcm_handle, mSampleBfr1 + written, n - written);
}
N += written;
Nonblocking writes
A nonblocking write may return less bytes than requested, if the audio buffer is full. If that's the case, errno is set to EAGAIN. If errno isn't EAGAIN, check the status and reprepare similar to the blocking write.

Handling audio buffer underflows

Before your first write, and any time the driver underruns, you must prepare the channel to ensure that everything is ready for you to start playing (or capturing) data. During a prepare, all the data stored in the driver that's related to snd_pcm_channel_status() is reset. Most importantly scount, the rolling count of bytes written to the device, is set to zero.

When an underrun occurs, it stalls the driver. No data is processed, and all writes fail until the channel is reprepared.

Closing the audio device

You close the audio device by calling snd_pcm_close(). You can optionally flush or drain the buffer.

Flushing vs Draining

There are two methods that you can use to ensure that the hardware buffers are completely empty:

flush
The flush calls (snd_pcm_channel_flush() and snd_pcm_plugin_flush()) don't return until all of the data in the buffers have been played out (unless an error occurs). If you're using the plugin interface and block mode, the flush calls fill the remainder of the partial fragment with silence so that all of the data can be played. It also returns the number of bytes that it used to silence pad, so you can adjust your internal counts.

Since the regular interface doesn't allow for partial writes, silence padding is meaningless. It simply returns zero for success after all data is played.

The flush calls regularly appear before the channel is closed to ensure that no data is left unplayed.

drain
The drain calls (snd_pcm_playback_drain() and snd_pcm_plugin_playback_drain()) discard any data that's waiting in the hardware buffers. This is useful for implementing pause buttons or for switching audio tracks midstream due to user input. The data contained in the hardware buffers adds latency. This can be very noticeable if the buffers are large. Drain allows you to cut the sound off immediately.
n = snd_pcm_plugin_flush (pcm_handle, SND_PCM_CHANNEL_PLAYBACK);

rtn = snd_mixer_close (mixer_handle);
rtn = snd_pcm_close (pcm_handle);

[Previous] [Contents] [Next]