Mixer Architecture

This chapter includes:

You can usually build an audio mixer from a relatively small number of components. Each of these components performs a specific mixing function. A summary of these components or elements follows:

Input
A connection point where an external analog signal is brought into the mixer.
Output
A connection point where an analog signal is taken from the mixer.
ADC
An element that converts analog signals to digital samples.
DAC
An element that converts digital samples to analog signals.
Switch
An element that can connect two or more points together. A simple switch may be used as a mute control. More complicated switches can mute the channels of a stream individually, or can even form crossbar matrices where n input signals can be connected to n output signals.
Volume
An element that adjusts the amplitude level of a signal by applying attenuation or gain.
Accumulator
An element the adds all signals input to it and produces an output signal.
Multiplexer
An element that allows the signal on one of its inputs to become its output.

By using these elements you can build a simple sound card mixer:


Simple sound card mixer


A simple sound card mixer.

In the diagram, the mute figures are switches, and the MIC and CD are input elements. This diagram is in fact a simplified representation of the Audio Codec '97 mixer, one of the most common mixers found on sound cards.

It's possible to control these mixer elements directly using the snd_mixer_element_read() and snd_mixer_element_write() functions, but this method isn't recommended because:

The element interface is the lowest level of control for a mixer and is complicated to control. One solution to this complexity is to arrange elements that are associated with a function into a mixer group. To further refine this idea, groups are classified as either playback or capture groups. To simplify creating and managing groups, a hard set of rules was developed for how groups are built from elements:

If you apply these rules to the simple mixer in the above diagram, you get the following:

Playback Group PCM
Elements B (volume) and C (switch).
Playback Group MIC
Elements E (volume) and F (switch).
Playback Group CD
Elements L (volume) and M (switch).
Playback Group MASTER
Elements H (volume) and I (switch).
Capture Group MIC
Element N (multiplexer); there's no volume or switch.
Capture Group CD
Element N (multiplexer); there's no volume or switch.
Capture Group INPUT
Elements O (volume) and P (switch).

In separating the elements into groups, you've reduced the complexity of control (there are 7 groups instead of 17 elements), and each group associates well with what applications want to control.

Opening the mixer device

To open a connection to the mixer device, call snd_mixer_open(). This call has arguments for selecting the card and mixer device number to open. Most sound cards have only one mixer, but there may be additional mixers in special cases.

The snd_mixer_open() call returns a mixer handle that you'll use as an argument for additional API calls applied to this device. It's a pointer to a snd_mixer_t structure, which is an opaque data type.

Controlling a mixer group

The best way to control a mixer group is to use the read-modify-write technique. Using this technique, you can examine the group capabilities and ranges before adjusting the group.

The first step in reading the properties and settings of a mixer group is to identify the group. Every mixer group has a name, but because two groups may have the same name, a name alone isn't enough to identify a specific mixer group. In order to make groups unique, mixer groups are identified by the combination of name and index. The index is an integer that represents the instance number of the name. In most cases, the index is 0; in the case of two mixer groups with the same name, the first has an index of 0, and the second has an index of 1.

To read a mixer group, call the snd_mixer_group_read() function. The arguments to this function are the mixer handle and the group control structure. The group control structure is of type snd_mixer_group_t; for details about its members, see the Audio Library chapter.

To read a particular group, you must set its name and index in the gid substructure (see snd_mixer_gid_t) before making the call. If the call to snd_mixer_group_read() succeeds, the function fills in the structure with the group's capabilities and current settings.

Now that you have the group capabilities and current settings, you can modify them before you write them back to the mixer group.

To write the changes to the mixer group, call snd_mixer_group_write(), passing as arguments the mixer handle and the group control structure.

The best mixer group with respect to your PCM subchannel

In a typical mixer, there are many playback mixer group controls, and possibly several that will control the volume and mute of the stream your application is playing.

For example, consider the Sound Blaster Live playing a wave file. Three playback mixer controls adjust the volume of the playback: Master, PCM, and PCM Subchannel. Although each of these groups can control the volume of our playback, some aren't specific to just our stream, and thus have more side effects.

As an example, consider what happens if you increase your wave file volume by using the Master group. If you do this, any other streams — such a CD playback — are affected as well. So clearly, the best group to use is the PCM subchannel, as it affects only your stream. However, on some cards, a subchannel group might not exist, so you need a better method to find the best group.

The best way to figure out which is the best group for a PCM subchannel is to let the driver (i.e., the driver author) do it. You can obtain the identity of the best mixer group for a PCM subchannel by calling snd_pcm_channel_setup() or snd_pcm_plugin_setup(), as shown below:

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)
{
    return -1;
}

Note: You must initialize the setup structure to zero and then set the mixer_gid pointer to a storage location for the group identifier.

One thing to note is that the best group may change, depending on the state of the PCM subchannel. Remember that the PCM subchannels aren't allocated to a client until the parameters of the channel are established. Similarly, the subchannel mixer group isn't available until the subchannel is allocated. Using the example of the Sound Blaster Live, the best mixer group before the subchannel is allocated is the PCM group and, after allocation, the PCM Subchannel group.

Finding all mixer groups

You can get a complete list of mixer groups by calling snd_mixer_groups(). You usually make this call twice, once to get the total number of mixer groups, then a second time to actually read their IDs. The arguments to the call are the mixer handle and a snd_mixer_group_t structure. The structure contains a pointer to where the groups' identifiers are to be stored (an array of snd_mixer_gid_t structures), and the size of that array. The call fills in the structure with how many identifiers were stored, and indicates if some couldn't be stored because they would exceed the storage size.

Here's a short example (the snd_strerror() prints error messages for the sound functions):

while (1)
{
    memset (&groups, 0, sizeof (groups));
    if ((ret = snd_mixer_groups (mixer_handle, &groups) < 0))
    {
       fprintf (stderr, "snd_mixer_groups API call - %s",
                snd_strerror (ret));
    }

    mixer_n_groups = groups.groups_over;
    if (mixer_n_groups > 0)
    {
        groups.groups_size = mixer_n_groups;
        groups.pgroups = (snd_mixer_gid_t *) malloc (
           sizeof (snd_mixer_gid_t) * mixer_n_groups);

        if (groups.pgroups == NULL)
            fprintf (stderr, "Unable to malloc group array - %s",
                     strerror (errno));

        groups.groups_over = 0;
        groups.groups = 0;

        if (snd_mixer_groups (mixer_handle, &groups) < 0)
            fprintf (stderr, "No Mixer Groups ");

        if (groups.groups_over > 0)
        {
            free (groups.pgroups);
            continue;
        }
        else
        {
            printf ("sorting GID table \n");
            snd_mixer_sort_gid_table (groups.pgroups, mixer_n_groups,
                snd_mixer_default_weights);
            break;
        }
    }
}

Mixer event notification

By default, all mixer applications are required to keep up-to-date with all mixer changes. This is done by enqueuing a mixer-change event on all applications other than the application making a change. The driver enqueues these events on all applications that have an open mixer handle, unless the application uses the snd_mixer_set_filter() API call to mask out events it's not interested in.

Applications use the snd_mixer_read() function to read the enqueued mixer events. The arguments to this function are the mixer handle and a structure of callback functions to call based on the event type.

You can use the select() function (see the QNX Library Reference) to determine when to call snd_mixer_read(). To get the file descriptor to pass to select(), call snd_mixer_file_descriptor().

Here's a short example:

static void mixer_callback_group (void *private_data,
                                  int cmd,
                                  snd_mixer_gid_t * gid)
{
    switch (cmd)
    {
    case SND_MIXER_READ_GROUP_VALUE:
        printf ("Mixer group %s %d changed value \n",
                gid->name, gid->index);
        break;

    case SND_MIXER_READ_GROUP_ADD:
        break;

    case SND_MIXER_READ_GROUP_REMOVE:
        break;
    }
}

int mixer_update (int fd, void *data, unsigned mode)
{
    snd_mixer_callbacks_t callbacks = { 0, 0, 0, 0 };

    callbacks.group = mixer_callback_group;
    snd_mixer_read (mixer_handle, &callbacks);
    return (Pt_CONTINUE);
}

int main (void)
{
    snd_mixer_t *mixer_handle;
    int ret;

    if ((ret = snd_mixer_open (&mixer_handle, 0, 0) < 0))
        printf ("Unable to open/read mixer - %s",
                snd_strerror (ret));

    PtAppAddFd (NULL,
                snd_mixer_file_descriptor (mixer_handle),
                Pt_FD_READ, mixer_update, NULL);
    ...
}

Closing the mixer device

To close the mixer handle, simply call snd_mixer_close().