Set up the libasound audio mixer and PCM components

To abstract the libasound and PCM setup, we can create the setup_snd() function, which performs all of the setup operations. To set up these components, we use the libasound library.

Set up the libasound mixer and PCM components

Let's begin with the signature of our sound setup function.

/*
 * General setup of the libasound audio mixer and pcm components.
 * Some of the settings are based on the format of the wav data.
 */
int 
setup_snd(const char * name)
{

The function accepts a parameter, const char *name, which is the name of the device to set up. If the parameter is NULL, the default value is "pcmPreferred". The function returns SUCCESS if it is successful, otherwise it returns FAILURE.

Now, we set up the libasound mixer and PCM components in our function. First, we declare the following variables: an integer value for the size of the audio fragment, an integer value for the number of fragments, an integer value for the return value, and a character array for the device name.

int fragsize = -1;
int num_frags = -1;
int rtn;
char *dev_name;

Now we set the name of the device based on the parameter. If the parameter is NULL, use "pcmPreferred".

if (NULL == name) {
    dev_name = "pcmPreferred";
} else {
    dev_name = (char *) name;
}

Next we open the PCM device handle using snd_pcm_open_name(). We pass a pointer to the snd_pcm_t structure and the snd_pcm_open_name function fills this structure with the appropriate information. If the return value from the function is less than 0, we have an error case. In an error case, we display the error to the user and exit with an error condition.

if ((rtn = snd_pcm_open_name(&pcm_handle, dev_name,
                             SND_PCM_OPEN_PLAYBACK)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_pcm_open_name failed: %s\n",
             snd_strerror(rtn));
    show_dialog_message (msg);
    return FAILURE;
}

Now we obtain the capabilities of the PCM device using snd_pcm_info(). We can access the PCM information using structure variables. If the function returns a value less than 0, we have an error case. In an error case, we display the error to the user, close the PCM handle, and exit with an error condition. Then we set our global card variable to the sound card number.

if ((rtn = snd_pcm_info(pcm_handle, &info)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_pcm_info failed: %s\n",
             snd_strerror(rtn));
    goto setup_failure;
}
card = info.card;

Next, we can disable memory-mapped input and output (which maps files or devices into memory). This step isn't required for the PlayWav sample, but we include it here in case it's required for your program. If the snd_pcm_plugin_set_disable() function returns a value less than 0, we have an error case. In an error case, we display the error to the user, close the PCM handle, and exit with an error condition.

/* Disabling mmap is not actually required in this example but it is included to     
 * demonstrate how it is used when it is required.
 */
if ((rtn = snd_pcm_plugin_set_disable(pcm_handle,
                                      PLUGIN_DISABLE_MMAP)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_pcm_plugin_set_disable failed: %s\n",
             snd_strerror(rtn));
    goto setup_failure;
}

We use memset() to set all of the values in the typedef structure snd_pcm_channel_info_t to 0. Then, we set the channel information to playback mode using SND_PCM_CHANNEL_PLAYBACK. Next, we obtain channel information using the snd_pcm_plugin_info() function. An audio channel is a separate stream of audio information. Examples of such channel systems include mono audio devices, which have one channel, stereo sources, which have two channels (left and right), and so on. If the function returns a value less than 0, we have an error case. In an error case, we display the error to the user, close the PCM handle, and exit with an error condition.

memset(&pi, 0, sizeof(pi));
pi.channel = SND_PCM_CHANNEL_PLAYBACK;
if ((rtn = snd_pcm_plugin_info(pcm_handle, &pi)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_pcm_plugin_info failed: %s\n",
             snd_strerror(rtn));
    goto setup_failure;
}

Now we clear the snd_pcm_channel_params_t structure using the memset() function. Then we configure the PCM parameter information based on .wav file format. To do so, we set the channel direction (to play the audio file instead of recording new audio), channel mode, start mode (to start playing audio when the whole queue is filled), stop mode (to stop playing audio when an underrun or overrun occurs), audio fragment size, maximum audio fragment size, and the number of audio fragments. An underrun occurs if data is written at lower speed than it's being read, and an overrun occurs if data is written at higher speed than it's being read.

Then we set format information based on the .wav format that we obtained for interleaving samples, the requested rate in Hz, the number of audio channels, and the format (some form of compression or normal PCM format). If we use a variable from our .wav header structure, we must convert the value to little-endian format using the ENDIAN_LE<bits used for type>() macro. Then we set the name of the sw_mixer subchannel group to the wave playback channel. Finally, we call the snd_pcm_plugin_params() function to obtain additional information that we couldn't determine using the .wav format. If this function returns a value less than 0, we have an error case. In an error case, we display the error to the user, close the PCM handle, and exit with an error condition.

memset(&pp, 0, 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;

if (fragsize != -1) {
    pp.buf.block.frag_size = fragsize;
}

pp.buf.block.frags_max = num_frags;
pp.buf.block.frags_min = 1;

pp.format.interleave = 1;
pp.format.rate = sample_rate;
pp.format.voices = sample_channels;

if (ENDIAN_LE16(wav_header.format_tag) == 6)
    pp.format.format = SND_PCM_SFMT_A_LAW;
else if (ENDIAN_LE16(wav_header.format_tag) == 7)
    pp.format.format = SND_PCM_SFMT_MU_LAW;
else if (sample_bits == 8)
    pp.format.format = SND_PCM_SFMT_U8;
else if (sample_bits == 24)
    pp.format.format = SND_PCM_SFMT_S24;
else
    pp.format.format = SND_PCM_SFMT_S16_LE;

strcpy(pp.sw_mixer_subchn_name, "Wave playback channel");
if ((rtn = snd_pcm_plugin_params(pcm_handle, &pp)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_pcm_plugin_params failed: %s\n",
             snd_strerror(rtn));
    goto setup_failure;
}

Now we prepare the hardware for playback by using the snd_pcm_plugin_prepare() function. You can use the plugin interface for format conversion to or from the original format. Again, if this function returns a value less than 0, we have an error case. In an error case, we display the error to the user, close the PCM handle, and exit with an error condition.

if ((rtn = snd_pcm_plugin_prepare(pcm_handle,
                                  SND_PCM_CHANNEL_PLAYBACK)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_pcm_plugin_prepare failed: %s\n",
             snd_strerror(rtn));
    goto setup_failure;
}

Now we set up the setup and group structures. First, clear both structures using the memset() function. Then set the setup.channel to playback mode using the SND_PCM_CHANNEL_PLAYBACK macro. Set the mixer group ID in the setup structure to point to the mixer group ID memory location in the group structure, so when we obtain the mixer group information, our PCM channel configuration structure already references the appropriate mixer group. Then we call snd_pcm_plugin_setup() to fill the setup structure with information about the current configuration of the PCM channel. If this function returns a value less than 0, we have an error case. In an error case, we display the error to the user, close the PCM handle, and exit with an error condition.

Note that snd_pcm_plugin_setup() selects the most appropriate mixer group regarding the PCM subchannel. Then, we check that the mixer group ID name is set by checking if the first character, group.gid.name[0], is equal to 0. If the name is not set, we have an error case, so display the error to the user, close the PCM handle, and exit with an error condition.

memset(&setup, 0, sizeof(setup));
memset(&group, 0, sizeof(group));
setup.channel = SND_PCM_CHANNEL_PLAYBACK;
setup.mixer_gid = &group.gid;

if ((rtn = snd_pcm_plugin_setup(pcm_handle, &setup)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_pcm_plugin_setup failed: %s\n",
             snd_strerror(rtn));
    goto setup_failure;
}

if (group.gid.name[0] == 0) {
    snprintf(msg, MSG_SIZE, "Mixer Pcm Group [%s] Not Set \n",
             group.gid.name);
    goto setup_failure;
}

The last step in setting up the libasound mixer and PCM components is opening a handle to the mixer using the snd_mixer_open() function. We are passing a pointer to the snd_mixer_t structure, which the function populates. We also pass the sound card number and mixer device for this subchannel to the function. Again, if this function returns a value less than 0, we have an error case. In an error case, we display the error to the user, close the PCM handle, and exit with an error condition.

if ((rtn = snd_mixer_open(&mixer_handle, card,
                          setup.mixer_device)) < 0) {
    snprintf(msg, MSG_SIZE, "snd_mixer_open failed: %s\n",
             snd_strerror(rtn));
    goto setup_failure;
}

Now that we've set up everything for the libasound mixer and PCM components, let's display some of the information that we obtained to the user.

char tmp[MSG_SIZE];
snprintf(msg, MSG_SIZE, "Format %s \n", snd_pcm_get_format_name(setup.format.format));
snprintf(tmp, MSG_SIZE, "Frag Size %d \n", setup.buf.block.frag_size);
strlcat(msg, tmp, MSG_SIZE);
snprintf(tmp, MSG_SIZE, "Total Frags %d \n", setup.buf.block.frags);
strlcat(msg, tmp, MSG_SIZE);
snprintf(tmp, MSG_SIZE, "Rate %d \n", setup.format.rate);
strlcat(msg, tmp, MSG_SIZE);
snprintf(tmp, MSG_SIZE, "Voices %d \n", setup.format.voices);
strlcat(msg, tmp, MSG_SIZE);
snprintf(tmp, MSG_SIZE, "Mixer Pcm Group [%s]\n", group.gid.name);
strlcat(msg, tmp, MSG_SIZE);
show_dialog_message(msg);

return SUCCESS;

For error cases, we display a message to the user, close the PCM handle, and exit with a failure condition.

setup_failure:
    show_dialog_message (msg);
    snd_pcm_close(pcm_handle);
    return FAILURE;
}

Local scope

Next, let's create the main() function:

int
main(int argc, char **argv)
{

Next, declare the number of samples as an integer and the sample buffer as a character pointer (the size of one character is 1 byte).

FILE *file;
int samples;
char *sample_buffer;

Now we declare integer variables for return values, buffer size, bytes read, and the total number of bytes written. Then, we declare the file descriptor sets for read and write. A file descriptor is an abstract indicator for accessing a file.

int rtn, final_return_code = -1, exit_application = 0;

int bsize, bytes_read, total_written = 0;
fd_set rfds, wfds;

Last, declare character arrays for the .wav file and the current working directory. The size of each array is set to PATH_MAX, which specifies the maximum path size for the OS.

char input_file[PATH_MAX];
char cwd[PATH_MAX];

Additional components

As we mentioned earlier, this tutorial focuses on the .wav audio aspects of the PlayWav sample app. Here, we briefly discuss how to handle navigator and dialog events and how to set up dialog boxes.

Request navigator events

To handle navigator events in our application, we must initialize the BlackBerry Platform Services (BPS) and then request navigator events by calling navigator_request_events(). If the value returned from the function is not BPS_SUCCESS, we have an error case and we need to exit with a failure condition.

/*     
 * Before we can listen for events from the BlackBerry(R) 10 OS platform    
 * services, we need to initialize the BPS infrastructure
 */

 bps_initialize();  

 if (setup_screen() != EXIT_SUCCESS) {
        printf("Unable to set up the screen. Exiting.");
        exit(-1);
 }

   /*     
    * Once the BPS infrastructure has been initialized we can register for     
    * events from the various BlackBerry(R) 10 OS platform services. The     
    * Navigator service manages and delivers application life cycle and     
    * visibility events.
    *     
    * For this sample, we request Navigator events so we can track when    
    * the system is terminating the application (NAVIGATOR_EXIT event).    
    * This allows us to clean up application resources.    
    *
    * We request Audio Device events because we want to make sure that    
    * we properly handle changes in audio output.
    *     
    * We request dialog events to properly initialize the dialog    
    * subsystem in order to display status and error messages.  
    */
 if (BPS_SUCCESS != navigator_request_events(0)) {
    fprintf(stderr, "Error requesting navigator events: %s",
            strerror(errno));
    exit(-1);
 }
For more information on navigator events, see the API reference .

Handle dialog boxes

To set up and display dialog boxes, the PlayWav sample app provides functions in dialogutil.h and dialogutil.c. Go ahead and copy these two files from the sample into your project. We don't need to create these files. We reuse the utilities from the sample app. Here, we describe these functions:

  • int setup_screen()

    This function sets up the screen by doing the following:

    • Gets a handle for the screen
    • Creates context with the screen
    • Creates a window for the screen
    • Gets a window buffer
    • Sets the window group ID for the window
  • static char* get_window_group_id()

    This function obtains the window group ID based on the process ID of the app. A character string of the window group ID is returned.

  • void cleanup_screen()

    This function destroys and clears the window and context handles.

  • void create_dialog()

    This function creates an alert dialog box.

  • void show_dialog_message(const char* msg)

    This function sets the message text for the alert dialog box and updates the display. This function also displays the error messages to the standard error stream (STDERR).

  • void destroy_dialog()

    This function destroys the dialog box.

  • int err(char *message)

    This function is defined in main.c, and it displays the error message to the user in the dialog box that we set up with create_dialog().

To display status and error messages, we must initialize the dialog box subsystem. To receive the dialog box events, we call dialog_request_events(0). If this function doesn't return BPS_SUCCESS, we have an error case, so we exit with a failure condition.

if (BPS_SUCCESS != dialog_request_events(0)) {
    fprintf(stderr, "Error requesting dialog events: %s",
            strerror(errno));
    exit(-1);
}

For more information about dialog boxes, see the API reference . For more information about the functions above, see the dialogutil.c file.

Handle audio device events

In main(), after we call bps_initialize(), we can request to receive audio device events from the BlackBerry Platform Services infrastructure by calling audiodevice_request_events(0). We need to receive audio events to handle changes in the audio output. If this function doesn't return BPS_SUCCESS, we have an error case, so we exit with a failure condition.

if (BPS_SUCCESS != audiodevice_request_events(0)) {
    fprintf(stderr, "Error requesting audio device events: %s",
            strerror(errno));
    exit(-1);
}

Obtain the .wav file path

Now, we get the current working directory for the app by using getcwd(char*, size_t), which is declared in unistd.h. Then we combine the current working directory with the relative path of the .wav file and store it in the input_file variable. To combine the two character arrays, we use the snprintf() function. This function returns the number of characters outputted (which is the total file path), less the null-terminating character. If that number of characters is greater than PATH_MAX (which is defined in limits.h), then our input file location is too large and we have an error case.

/*     
 * Create and display the dialog.
 */
  create_dialog(); 

/*    
 * Open and check the input file.
 */
getcwd(cwd, PATH_MAX);
rtn = snprintf(input_file, PATH_MAX, "%s/%s", cwd, WAV_RELATIVE_PATH);
if (rtn > PATH_MAX - 1) {
    err("File name and path too long");
    goto fail1;
}

Open the .wav file

To open the .wav file, we call fopen(), which is declared in stdio.h. If the file pointer returned is NULL or 0, we have an error case. In an error case, we want to display a message to the user, destroy the dialog box, clean up resources, close the BlackBerry Platform Services infrastructure to stop receiving events, and exit with a failure condition.

if ((file = fopen(input_file, "r")) == 0) {
    err("File open failed");
    goto fail1;
}

In our calling function, we call check_hdr(). If our function returns a negative value, we have an error case.

if (check_hdr(file) == -1) {
    err("check_hdr failed");
    goto fail2;
}

/*     
 * Parse the headers in the wav file to figure out what kind of data we     
 * are dealing with in the file.
 */

If our function doesn't return a negative value, we have a valid .wav file and we can begin to parse it.

Handle the format chunk

samples = find_tag(file, "fmt ");
fread(&wav_header, sizeof(wav_header), 1, file);
fseek(file,(samples - sizeof(wave_hdr)), SEEK_CUR);

Now that we have our .wav information, let's display some information to the user. Remember that you must convert the variables to little-endian format.

sample_rate = ENDIAN_LE32(wav_header.samples_per_sec);
sample_channels = ENDIAN_LE16(wav_header.channels);
sample_bits = ENDIAN_LE16(wav_header.bits_per_sample);

snprintf(msg, MSG_SIZE, "SampleRate = %d, channels = %d,
    SampleBits = %d\n", sample_rate, sample_channels,
    sample_bits);
 show_dialog_message (msg);

Call our setup_snd() function

Now that we've written setup_snd(), we call this function from our calling function using the default name, "pcmPreferred". If the return value is FAILURE, we have an error case. If an error case occurs at this point, we close the .wav file, destroy the dialog box, clean up the screen, close the BlackBerry Platform Services infrastructure to stop receiving events, and exit with an error code.

if (setup_snd(NULL)) {
    goto fail2;
}

Last modified: 2015-03-31



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus