Rockchip RK3399 – ASoC Sound Card Control Devices & Kcontrol

—————————————————————————————————————————-

Development Board: NanoPC-T4 Development BoardeMMC: 16GBLPDDR3: 4GBDisplay: 15.6-inch HDMI Interface Displayu-boot: 2023.04Linux: 6.3—————————————————————————————————————————-

Control is an abstraction used in audio drivers to represent user-operable audio parameters or functions. It can be volume control, mixer control, switch control, etc. Control provides a unified interface that allows users to manage and adjust audio parameters through audio device drivers.

The ALSA CORE has implemented the Control middleware, defining all Control APIs in include/sound/control.h. If you want to implement your own controls for your Codec, please include this header file in your code.

It is important to note that the Control in Control devices refers to controlling; while the controls/control/kcontrol mentioned later refers to the controls, which mainly implement controls such as volume and mixing for the sound card, and can be understood as switches.

1. Control Devices

1.1 Creating Control Devices

Control devices, like PCM devices, are logical devices under the sound card. User space applications access this Control device through alsa-lib, reading or controlling the control states of the controls, thus achieving various Mixer control operations for the audio Codec.

The process of creating Control devices is largely similar to that of creating PCM devices. For creating PCM devices, you only need to actively call the snd_pcm_new function during driver initialization, while Control devices are created using snd_ctl_create. However, since the snd_card_create function already calls the snd_ctl_create function to create Control devices, we do not need to explicitly create Control devices; as long as we establish the sound card, Control devices are automatically created.

Let’s take a look at what snd_ctl_create actually does. The function is defined in sound/core/control.c;

/*
 * create control core:
 * called from init.c
 */
int snd_ctl_create(struct snd_card *card)       // Pass in the sound card device
{
        static struct snd_device_ops ops = {     // Sound card Control device operation set
                .dev_free = snd_ctl_dev_free,
                .dev_register = snd_ctl_dev_register,
                .dev_disconnect = snd_ctl_dev_disconnect,
        };
        int err;

        if (snd_BUG_ON(!card))
                return -ENXIO;
        if (snd_BUG_ON(card->number < 0 || card->number >= SNDRV_CARDS))  // Invalid sound card device number
                return -ENXIO;

        snd_device_initialize(&card->ctl_dev, card);               // Initialize card->ctl_dev control device, set class to sound_class, parent to card->card_dev
        dev_set_name(&card->ctl_dev, "controlC%d", card->number);  // Allocate a name for the control device controlC%d

        err = snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);  // Create a new snd_device instance and add it to the sound card device's devices linked list
        if (err < 0)
                put_device(&card->ctl_dev);
        return err;
}

This function mainly does two things:

  • • Calls snd_device_initialize to initialize the sound card’s control device, which is card->ctrl_dev; here, it sets the control device’s parent to the sound card device card->card_dev and class to sound_class;
  • • Calls snd_device_new to allocate a snd_device instance for the control device and adds it to the sound card device’s logical device linked list;
1.1.1 snd_device_initialize

The snd_device_initialize function is defined in sound/core/init.c, used to initialize various member variables of the struct device structure and allocate appropriate resources;

/**
 * snd_device_initialize - Initialize struct device for sound devices
 * @dev: device to initialize
 * @card: card to assign, optional
 */
void snd_device_initialize(struct device *dev, struct snd_card *card)
{
        device_initialize(dev);
        if (card)
                dev->parent = &card->card_dev;
        dev->class = sound_class;
        dev->release = default_release;
}
1.1.2 snd_ctl_dev_register

When registering the sound card device card, it will traverse the logical device linked list devices of the sound card device and call the dev_register function in the sound card logical device operation set, which for Control devices is the snd_ctl_dev_register function;

Finally, let’s take a look at the operation set of Control devices:

static struct snd_device_ops ops = {    
        .dev_free = snd_ctl_dev_free,
        .dev_register = snd_ctl_dev_register,
        .dev_disconnect = snd_ctl_dev_disconnect,
};

These callback functions are all defined in sound/core/control.c. Taking the snd_ctl_dev_register function as an example, this callback function establishes the device file node used for communication with user space applications (alsa-lib): /dev/snd/controlC%d;

/*
 * registration of the control device
 */
static int snd_ctl_dev_register(struct snd_device *device)
{
        struct snd_card *card = device->device_data;

        return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,       // Register Control device
                                   &snd_ctl_f_ops, card, &card->ctl_dev);
}

dev_free, dev_disconnect we are not particularly concerned with, so we can ignore them:

/*
 * disconnection of the control device
 */
static int snd_ctl_dev_disconnect(struct snd_device *device)
{
        struct snd_card *card = device->device_data;
        struct snd_ctl_file *ctl;

        read_lock(&card->ctl_files_rwlock);     // Read-write spin lock
        list_for_each_entry(ctl, &card->ctl_files, list) {  // Traverse ctl_files linked list
                wake_up(&ctl->change_sleep);
                kill_fasync(&ctl->fasync, SIGIO, POLL_ERR);
        }
        read_unlock(&card->ctl_files_rwlock);  // Release lock

        return snd_unregister_device(&card->ctl_dev);  // Unload sound card logical device
}

/*
 * free all controls
 */
static int snd_ctl_dev_free(struct snd_device *device)
{
        struct snd_card *card = device->device_data;
        struct snd_kcontrol *control;

        down_write(&card->controls_rwsem);     // Acquire read-write semaphore
        while (!list_empty(&card->controls)) {  // Traverse control linked list
                control = snd_kcontrol(card->controls.next);
                snd_ctl_remove(card, control); 
        }
        up_write(&card->controls_rwsem);   // Release read-write semaphore
        put_device(&card->ctl_dev);
        return 0;
}

2. kcontrol

kcontrol is actually a type of control, mainly implementing control of the sound card’s volume, mixing, and other series of controls, which can be understood as a switch.

The corresponding data structure for kcontrol is snd_kcontrol_new, and these snd_kcontrol_new structures will be registered in the system during the sound card initialization phase through the snd_soc_add_component_controls function, allowing user space to view and set the state of these controls through tools like amixer or alsamixer.

In fact, in addition to the snd_kcontrol_new structure, there is also a snd_kcontrol structure; snd_kcontrol_new is more like a template for kcontrol, while snd_kcontrol is the real kcontrol.

The steps for creating controls are as follows: (1) Define an array of snd_kcontrol_new;

(2) Create and add multiple kcontrols to the sound card’s controls linked list using snd_soc_add_component_controls based on the snd_kcontrol_new array;

2.1 Data Structures

2.1.1 snd_kcontrol_new

The struct snd_kcontrol_new is defined in include/sound/control.h:

struct snd_kcontrol_new {
        snd_ctl_elem_iface_t iface;     /* interface identifier */
        unsigned int device;            /* device/client number */
        unsigned int subdevice;         /* subdevice (substream) number */
        const char *name;               /* ASCII name of item */
        unsigned int index;             /* index of item */
        unsigned int access;            /* access rights */
        unsigned int count;             /* count of same elements */
        snd_kcontrol_info_t *info;
        snd_kcontrol_get_t *get;
        snd_kcontrol_put_t *put;
        union {
                snd_kcontrol_tlv_rw_t *c;
                const unsigned int *p;
        } tlv;
        unsigned long private_value;
};

Where:

  • • iface: The type of control. ALSA defines several types (SNDDRV_CTL_ELEM_IFACE_XXX), the common type is MIXER; of course, it can also define a global CARD type or a type belonging to a certain class of devices, such as HWDEP, PCMRAWMIDI, TIMER, etc., in which case the device and subdevice fields need to specify the logical number of the sound card;
  • • name: The name of the control. From ALSA 0.9.x onwards, the name of the control has become quite important, as the function of the control is categorized by name. ALSA has predefined some control names, which we will discuss in detail in later chapters;
  • • index: The number of the control in the sound card. If there is more than one codec in the sound card with controls of the same name, we can distinguish these controls by index. If the index is 0, this distinction strategy can be ignored;
  • • access: The access type of the control. Each bit represents a type of access, and these access types can be combined with multiple “or” operations;
  • • private_value: Has different meanings depending on the type of control. For ordinary controls, the private_value field can be used to define the address of the register corresponding to the control and the position information of the corresponding control bit in the register;
  • • tlv: Control metadata;
  • • get: Used to retrieve the current state value of the control;
  • • put: Used to set the state value of the control;
2.1.2 Relationship Diagram

To better understand the relationship between struct snd_kcontrol_new, struct snd_kcontrol, and other data structures, we have drawn the following relationship diagram:

Rockchip RK3399 - ASoC Sound Card Control Devices & Kcontrol
2.1.3 Naming of Controls

The name of the control needs to follow certain standards, and can usually be defined in three parts: Source – Direction – Function.

  • • Source: Can be understood as the input end of the control. ALSA has predefined some common sources, such as: Master, PCM, CD, Line, I2S, Headphone, Speaker, Mic, etc.;
  • • Direction: Represents the data flow direction of the control, such as: Playback, Capture, Bypass, Bypass Capture, etc. It can also be left undefined, indicating that the control is bidirectional (playback and capture);
  • • Function: Depending on the function of the control, it can be one of the following strings: Switch, Volume, Route, etc.;

There are also some naming exceptions, such as Tone Control – Switch, Tone Control – Bass, which are no longer recommended by the official.

For more content, you can refer to the official manual: Standard ALSA Control Names.

2.1.4 Access Flags

The access field is a bitmask that saves the access type of the control. The default access type is: SNDDRV_CTL_ELEM_ACCESS_READWRITE, indicating that the control supports both read and write operations. If the access field is not defined (.access==0), it is also considered as READWRITE type.

If it is a read-only control, the access should be set to: SNDDRV_CTL_ELEM_ACCESS_READ, in which case we do not need to define the put callback function. Similarly, if it is a write-only control, the access should be set to: SNDDRV_CTL_ELEM_ACCESS_WRITE, and we do not need to define the get callback function.

If the value of the control changes frequently (for example: level meter), we can use the VOLATILE type, meaning that the control may change without notification, and the application should periodically query the value of the control.

2.1.5 Callback Functions

(1) info callback function

The info callback function is used to retrieve detailed information about the control. Its main job is to fill in the snd_ctl_elem_info data structure passed in through parameters. The following example is an info callback for a boolean control with a single element:

/**
 * snd_ctl_boolean_mono_info - Helper function for a standard boolean info
 * callback with a mono channel
 * @kcontrol: the kcontrol instance
 * @uinfo: info to store
 *
 * This is a function that can be used as info callback for a standard
 * boolean control with a single mono channel.
 *
 * Return: Zero (always successful)
 */
int snd_ctl_boolean_mono_info(struct snd_kcontrol *kcontrol,
                              struct snd_ctl_elem_info *uinfo)
{
        uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
        uinfo->count = 1;
        uinfo->value.integer.min = 0;
        uinfo->value.integer.max = 1;
        return 0;
}

Where:

  • • type: Indicates the value type of the control, which can be one of BOOLEAN, INTEGER, ENUMERATED, BYTES, IEC958, and INTEGER64;
  • • count: Indicates how many element units are included in the control. For example, the volume control for stereo has volume values for both left and right channels, so its count field equals 2;
  • • value: Is a union whose content is related to the type of control; where boolean and integer types are the same;

The ENUMERATED type is special. Its value needs to set a string and the index of the string. Please see the following example:

/**
 * snd_ctl_enum_info - fills the info structure for an enumerated control
 * @info: the structure to be filled
 * @channels: the number of the control's channels; often one
 * @items: the number of control values; also the size of @names
 * @names: an array containing the names of all control values
 *
 * Sets all required fields in @info to their appropriate values.
 * If the control's accessibility is not the default (readable and writable),
 * the caller has to fill @info->access.
 *
 * Return: Zero (always successful)
 */
int snd_ctl_enum_info(struct snd_ctl_elem_info *info, unsigned int channels,
                      unsigned int items, const char *const names[])
{
        info->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
        info->count = channels;
        info->value.enumerated.items = items;
        if (!items)
                return 0;
        if (info->value.enumerated.item >= items)
                info->value.enumerated.item = items - 1;
        WARN(strlen(names[info->value.enumerated.item]) >= sizeof(info->value.enumerated.name),
             "ALSA: too long item name '%s'\n",
             names[info->value.enumerated.item]);
        strscpy(info->value.enumerated.name,
                names[info->value.enumerated.item],
                sizeof(info->value.enumerated.name));
        return 0;
}

ALSA has already implemented some common info callback functions for us, such as: snd_ctl_boolean_mono_info, snd_ctl_boolean_stereo_info, snd_ctl_enum_info, etc.

(2) get callback function

The get callback function is used to read the current value of the control and return it to user space applications;

static int snd_myctl_get(struct snd_kcontrol *kcontrol,
    struct snd_ctl_elem_value *ucontrol)
{
    struct mychip *chip = snd_kcontrol_chip(kcontrol);
    ucontrol->value.integer.value[0] = get_some_value(chip);
    return 0;
}

The assignment of the value field depends on the type of control (similar to the info callback). Many sound card drivers utilize it to store the addresses of hardware registers, bit-shifts, and bit-masks; in this case, the private_value field can be set as follows:

private_value = reg | (shift << 16) | (mask << 24);

Then, the get callback function can be implemented as follows:

static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol,
    struct snd_ctl_elem_value *ucontrol)
{
    int reg = kcontrol->private_value & 0xff;
    int shift = (kcontrol->private_value >> 16) & 0xff;
    int mask = (kcontrol->private_value >> 24) & 0xff;
    ....

    // Read the corresponding register value and fill it into value
}

If the count field of the control is greater than 1, indicating that the control has multiple element units, the get callback function should also fill multiple values in the value.

(3) put callback function

The put callback function is used to set the control values from applications.

static int snd_myctl_put(struct snd_kcontrol *kcontrol,
    struct snd_ctl_elem_value *ucontrol)
{
    struct mychip *chip = snd_kcontrol_chip(kcontrol);
    int changed = 0;
    if (chip->current_value !=
        ucontrol->value.integer.value[0]) {
        change_current_value(chip,
        ucontrol->value.integer.value[0]);
        changed = 1;
    }
    return changed;
}

As shown in the above example, when the value of the control is changed, the put callback must return 1; if the value has not changed, it should return 0. If an error occurs, it should return a negative error code.

Like the get callback, when the count of the control is greater than 1, the put callback should also handle the element values in multiple controls.

2.2 Auxiliary Macro Definitions

The ASoC layer has already prepared a lot of macro definitions for us to define common controls, these macro definitions are located in include/sound/soc.h. Below, we will discuss how to use these preset macros to define some common controls.

2.2.1 Simple Control SOC_SINGLE

SOC_SINGLE can be considered the simplest control, which only has one control quantity, such as a switch, or a numerical variable (such as a frequency in a Codec, FIFO size, etc.). Let’s see how this macro is defined:

#define SOC_SINGLE(xname, reg, shift, max, invert) \
{       .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
        .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\
        .put = snd_soc_put_volsw, \
        .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }

The parameters of the macro definition are:

  • • xname: The name of the control;
  • • reg: The address of the register corresponding to the control;
  • • shift: The shift of the control bit in the register;
  • • max: The maximum value that can be set for the control;
  • • invert: Whether to logically negate the set value;

Here, another macro is used to define the private_value field: SOC_SINGLE_VALUE. Let’s take a look at its definition:

#define SOC_SINGLE_VALUE(xreg, xshift, xmax, xinvert, xautodisable) \
        SOC_DOUBLE_VALUE(xreg, xshift, xshift, xmax, xinvert, xautodisable)

Here, a soc_mixer_control structure is defined, and the address of this structure is assigned to the private_value field. The soc_mixer_control structure is as follows:

/* mixer control */
struct soc_mixer_control {
        int min, max, platform_max;
        int reg, rreg;
        unsigned int shift, rshift;
        unsigned int sign_bit;
        unsigned int invert:1;
        unsigned int autodisable:1;
#ifdef CONFIG_SND_SOC_TOPOLOGY
        struct snd_soc_dobj dobj;
#endif
};

It seems that soc_mixer_control is the true descriptor of the characteristics of the control, determining the address of the corresponding register, shift values, maximum value, and whether to logically negate, etc.

The put and get callback functions for the control need to use this structure to access the actual registers. Let’s see the definition of the get callback function snd_soc_get_volsw, which is located in sound/soc/soc-ops.c;

/**
 * snd_soc_get_volsw - single mixer get callback
 * @kcontrol: mixer control
 * @ucontrol: control element information
 *
 * Callback to get the value of a single mixer control, or a double mixer
 * control that spans 2 registers.
 *
 * Returns 0 for success.
 */
int snd_soc_get_volsw(struct snd_kcontrol *kcontrol,
        struct snd_ctl_elem_value *ucontrol)
{
        struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
        struct soc_mixer_control *mc =
                (struct soc_mixer_control *)kcontrol->private_value;
        unsigned int reg = mc->reg;
        unsigned int reg2 = mc->rreg;
        unsigned int shift = mc->shift;
        unsigned int rshift = mc->rshift;
        int max = mc->max;
        int min = mc->min;
        int sign_bit = mc->sign_bit;
        unsigned int mask = (1 << fls(max)) - 1;
        unsigned int invert = mc->invert;
        int val;
        int ret;

        if (sign_bit)
                mask = BIT(sign_bit + 1) - 1;

        ret = snd_soc_read_signed(component, reg, mask, shift, sign_bit, &val);
        if (ret)
                return ret;

        ucontrol->value.integer.value[0] = val - min;
        if (invert)
                ucontrol->value.integer.value[0] =
                        max - ucontrol->value.integer.value[0];

        if (snd_soc_volsw_is_stereo(mc)) {
                if (reg == reg2)
                        ret = snd_soc_read_signed(component, reg, mask, rshift,
                                sign_bit, &val);
                else
                        ret = snd_soc_read_signed(component, reg2, mask, shift,
                                sign_bit, &val);
                if (ret)
                        return ret;

                ucontrol->value.integer.value[1] = val - min;
                if (invert)
                        ucontrol->value.integer.value[1] =
                                max - ucontrol->value.integer.value[1];
        }
        return 0;
}

The above code is self-explanatory. It retrieves the soc_mixer_control structure from the private_value field and uses the information in this structure to access the corresponding registers and return the corresponding values.

2.2.2 SOC_SINGLE_TLV

This is an extension of SOC_SINGLE, mainly used to define controls that have gain control, such as volume controllers, EQ equalizers, etc.

#define SOC_SINGLE_TLV(xname, reg, shift, max, invert, tlv_array) \
{       .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
        .access = SNDRV_CTL_ELEM_ACCESS_TLV_READ |\
                 SNDRV_CTL_ELEM_ACCESS_READWRITE,\
        .tlv.p = (tlv_array), \
        .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\
        .put = snd_soc_put_volsw, \
        .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }

From the definition, it can be seen that the definition of the private_value field used to set register information is the same as that of SOC_SINGLE, and even the put and get callback functions use the same set. The only difference is the addition of a tlv_array parameter, which is assigned to the tlv.p field. User space can access the array pointed to by the tlv field by initiating the following two ioctl calls to the sound card’s Control device:

SNDRV_CTL_IOCTL_TLV_READ
SNDRV_CTL_IOCTL_TLV_WRITE
SNDRV_CTL_IOCTL_TLV_COMMAND

Typically, tlv_array is used to describe the mapping relationship between the set value of the register and its actual significance. The most common use case is to describe the mapping relationship between the set value and the corresponding dB value when used for volume controls. Please see the following example:

static const DECLARE_TLV_DB_SCALE(mixin_boost_tlv, 0, 900, 0);

static const struct snd_kcontrol_new wm1811_snd_controls[] = {
SOC_SINGLE_TLV("MIXINL IN1LP Boost Volume", WM8994_INPUT_MIXER_1, 7, 1, 0,
               mixin_boost_tlv),
SOC_SINGLE_TLV("MIXINL IN1RP Boost Volume", WM8994_INPUT_MIXER_1, 8, 1, 0,
               mixin_boost_tlv),
};

DECLARE_TLV_DB_SCALE is used to define a dB value mapping tlv_array. The above example shows that the type of this tlv is SNDRV_CTL_TLVT_DB_SCALE, the minimum value of the register corresponds to 0dB, and for each unit value increase in the register, the corresponding dB value increases by 9dB (0.01dB*900). The next two groups of SOC_SINGLE_TLV definitions show that we defined two boost controls, both with the register address WM8994_INPUT_MIXER_1, and the control bits are the 7th and 8th bits, with a maximum value of 1, so this control can only set two values 0 and 1, corresponding to dB values of 0dB and 9dB.

2.2.3 SOC_DOUBLE

Corresponding to SOC_SINGLE, the difference is that SOC_SINGLE only controls one variable, while SOC_DOUBLE can control two similar variables in one register simultaneously. The most common use case is for stereo controls, where we need to control both left and right channels simultaneously, hence there is an additional shift value parameter;

#define SOC_DOUBLE(xname, reg, shift_left, shift_right, max, invert) \
{       .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = (xname),\
        .info = snd_soc_info_volsw, .get = snd_soc_get_volsw, \
        .put = snd_soc_put_volsw, \
        .private_value = SOC_DOUBLE_VALUE(reg, shift_left, shift_right, \
                                          max, invert, 0) }

SOC_DOUBLE_R: Similar to SOC_DOUBLE, but for cases where the left and right channel control registers are different, SOC_DOUBLE_R is used to define, and the parameters need to specify two register addresses.

SOC_DOUBLE_TLV: The stereo version corresponding to SOC_SINGLE_TLV, usually used to define stereo volume controls.

SOC_DOUBLE_R_TLV: The SOC_DOUBLE_TLV version for cases where the left and right channels have independent register control.

2.2.4 Mixer Controls

Used for audio channel routing control, consisting of multiple inputs and a single output, where multiple inputs can be freely mixed together to form a mixed output: for example, when a phone call is made while playing music, the two data streams need to be mixed before outputting to the Speaker, which requires a Mixer;

Rockchip RK3399 - ASoC Sound Card Control Devices & Kcontrol

For Mixer controls, we can consider it as a combination of multiple simple controls. Typically, we will define a simple control for each input end of the Mixer to control the opening and closing of that input, which is reflected in the code as defining an array of soc_kcontrol_new:

static const struct snd_kcontrol_new wm8960_lin_boost[] = {
    SOC_SINGLE("LINPUT2 Switch", WM8960_LINPATH, 6, 1, 0),
    SOC_SINGLE("LINPUT3 Switch", WM8960_LINPATH, 7, 1, 0),
    SOC_SINGLE("LINPUT1 Switch", WM8960_LINPATH, 8, 1, 0),
};

The above Mixer uses the 6th, 7th, and 8th bits of the WM8960_LINPATH register to control the opening and closing of three input ends.

2.2.5 Mux Controls

Similar to Mixer controls, it is also a combination control of multiple input ends and a single output end. Unlike Mixer controls, Mux controls can only have one of the multiple input ends selected at the same time. Therefore, the registers corresponding to Mux controls can usually set a range of continuous values, with each different value corresponding to a different input end being opened. Unlike the above Mixer controls, ASoC uses the soc_enum structure to describe the register information of Mux controls:

/* enumerated kcontrol */
struct soc_enum {
        int reg;
        unsigned char shift_l;
        unsigned char shift_r;
        unsigned int items;
        unsigned int mask;
        const char * const *texts;
        const unsigned int *values;
        unsigned int autodisable:1;
#ifdef CONFIG_SND_SOC_TOPOLOGY
        struct snd_soc_dobj dobj;
#endif
};

Where:

  • • reg, reg2, shift_l, shift_r: Two register addresses and shift fields used to describe the control register information for the left and right channels;
  • • texts: Pointer to an array of strings used to describe the names corresponding to each input end;
  • • values: Points to an array defining the values that the register can select, with each value corresponding to an input end. If values are a set of continuous values, we can usually ignore the values parameter.

Next, let’s see how to define a Mux control: First, define the strings and values arrays. In the following example, since values are continuous, we do not need to define them:

static const char *drc_path_text[] = {
       "ADC",
       "DAC"
};

Second, use the auxiliary macro provided by ASoC to define the soc_enum structure to describe the registers:

static const struct soc_enum drc_path =
        SOC_ENUM_SINGLE(WM8993_DRC_CONTROL_1, 14, 2, drc_path_text);

Third, use the auxiliary macro provided by ASoC to define the soc_kcontrol_new structure, which is ultimately used to register this Mux control:

static const struct snd_kcontrol_new wm8993_snd_controls[] = {
  SOC_DOUBLE_TLV(......),
  ......
  SOC_ENUM("DRC Path", drc_path),
  ......
}

The above steps define a Mux control called DRC PATH, which has two input selections, ADC and DAC, controlled by the WM8993_DRC_CONTROL_1 register. Among them, the soc_enum structure is defined using the auxiliary macro SOC_ENUM_SINGLE, and the macro’s declaration is as follows:

#define SOC_ENUM_DOUBLE(xreg, xshift_l, xshift_r, xmax, xtexts) \
{       .reg = xreg, .shift_l = xshift_l, .shift_r = xshift_r, \
        .max = xmax, .texts = xtexts, \
        .mask = xmax ? roundup_pow_of_two(xmax) - 1 : 0}
#define SOC_ENUM_SINGLE(xreg, xshift, xmax, xtexts) \
        SOC_ENUM_DOUBLE(xreg, xshift, xshift, xmax, xtexts)

When defining the soc_kcontrol_new structure, the SOC_ENUM macro is used, and its definition is as follows:

#define SOC_ENUM(xname, xenum) \
{       .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,\
        .info = snd_soc_info_enum_double, \
        .get = snd_soc_get_enum_double, .put = snd_soc_put_enum_double, \
        .private_value = (unsigned long)&xenum }

The idea is so unified; it still uses the private_value field to record the soc_enum structure, but the callback functions have changed. Let’s take a look at the get callback corresponding to snd_soc_get_enum_double function:

/**
 * snd_soc_get_enum_double - enumerated double mixer get callback
 * @kcontrol: mixer control
 * @ucontrol: control element information
 *
 * Callback to get the value of a double enumerated mixer.
 *
 * Returns 0 for success.
 */
int snd_soc_get_enum_double(struct snd_kcontrol *kcontrol,
        struct snd_ctl_elem_value *ucontrol)
{
        struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
        struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;
        unsigned int val, item;
        unsigned int reg_val;

        reg_val = snd_soc_component_read(component, e->reg);
        val = (reg_val >> e->shift_l) & e->mask;
        item = snd_soc_enum_val_to_item(e, val);
        ucontrol->value.enumerated.item[0] = item;
        if (e->shift_l != e->shift_r) {
                val = (reg_val >> e->shift_r) & e->mask;
                item = snd_soc_enum_val_to_item(e, val);
                ucontrol->value.enumerated.item[1] = item;
        }

        return 0;
}

Through the info callback function, we can obtain the name corresponding to a certain input end, which is actually obtained from the texts array of the soc_enum structure:

/**
 * snd_soc_info_enum_double - enumerated double mixer info callback
 * @kcontrol: mixer control
 * @uinfo: control element information
 *
 * Callback to provide information about a double enumerated
 * mixer control.
 *
 * Returns 0 for success.
 */
int snd_soc_info_enum_double(struct snd_kcontrol *kcontrol,
        struct snd_ctl_elem_info *uinfo)
{
        struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;

        return snd_ctl_enum_info(uinfo, e->shift_l == e->shift_r ? 1 : 2,
                                 e->items, e->texts);
}

Next are several other macros commonly used to define Mux controls:

  • • SOC_VALUE_ENUM_SINGLE: Used to define soc_enum structure with a values field;
  • • SOC_VALUE_ENUM_DOUBLE: Stereo version of SOC_VALUE_ENUM_SINGLE;
2.2.6 Other Controls

In fact, in addition to the several common controls introduced above, ASoC also provides us with some other auxiliary definitions for controls. For detailed information, readers can refer to include/sound/soc.h. When defining get and put callbacks by yourself, you can use the following EXT versions:

  • • SOC_SINGLE_EXT;
  • • SOC_DOUBLE_EXT;
  • • SOC_SINGLE_EXT_TLV;
  • • SOC_DOUBLE_EXT_TLV;
  • • SOC_DOUBLE_R_EXT_TLV;
  • • SOC_ENUM_EXT.

2.3 Core APIs

2.3.1 snd_soc_cnew

When we have prepared all the above-discussed content, we can create our own kcontrol through the snd_soc_cnew function, which is located in sound/soc/soc-core.c and provided by the ASoC layer;

/**
 * snd_soc_cnew - create new control
 * @_template: control template
 * @data: control private data
 * @long_name: control long name
 * @prefix: control name prefix
 *
 * Create a new mixer control from a template control.
 *
 * Returns 0 for success, else error.
 */
struct snd_kcontrol *snd_soc_cnew(const struct snd_kcontrol_new *_template, // kcontrol template
                                  void *data, const char *long_name,
                                  const char *prefix)
{
        struct snd_kcontrol_new template;
        struct snd_kcontrol *kcontrol;
        char *name = NULL;

        memcpy(&template, _template, sizeof(template));
        template.index = 0;

        if (!long_name)
                long_name = template.name;

        if (prefix) {
                name = kasprintf(GFP_KERNEL, "%s %s", prefix, long_name);
                if (!name)
                        return NULL;

                template.name = name;
        } else {
                template.name = long_name;
        }

        kcontrol = snd_ctl_new1(&template, data);  // Key point

        kfree(name);

        return kcontrol;
}

Finally, it calls the ALSA layer’s snd_ctl_new1, which is located in sound/soc/control.c. The snd_ctl_new1 function allocates a kcontrol and copies the corresponding values from the ncontrol template to the kcontrol, so when defining ncontrol, we can typically add the __devinitdata prefix;

/**
 * snd_ctl_new1 - create a control instance from the template
 * @ncontrol: the initialization record
 * @private_data: the private data to set
 *
 * Allocates a new struct snd_kcontrol instance and initialize from the given
 * template.  When the access field of ncontrol is 0, it's assumed as
 * READWRITE access. When the count field is 0, it's assumes as one.
 *
 * Return: The pointer of the newly generated instance, or %NULL on failure.
 */
struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol,
                                  void *private_data)
{
        struct snd_kcontrol *kctl;
        unsigned int count;
        unsigned int access;
        int err;

        if (snd_BUG_ON(!ncontrol || !ncontrol->info))
                return NULL;

        count = ncontrol->count;
        if (count == 0)
                count = 1;

        access = ncontrol->access;
        if (access == 0)     // Set default access type
                access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
        access &= (SNDRV_CTL_ELEM_ACCESS_READWRITE |
                   SNDRV_CTL_ELEM_ACCESS_VOLATILE |
                   SNDRV_CTL_ELEM_ACCESS_INACTIVE |
                   SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE |
                   SNDRV_CTL_ELEM_ACCESS_TLV_COMMAND |
                   SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK |
                   SNDRV_CTL_ELEM_ACCESS_LED_MASK |
                   SNDRV_CTL_ELEM_ACCESS_SKIP_CHECK);

        err = snd_ctl_new(&kctl, count, access, NULL);  // Create a new snd_kcontrol and return via kctl;
        if (err < 0)
                return NULL;

        /* The 'numid' member is decided when calling snd_ctl_add(). */
        kctl->id.iface = ncontrol->iface;
        kctl->id.device = ncontrol->device;
        kctl->id.subdevice = ncontrol->subdevice;
        if (ncontrol->name) {
                strscpy(kctl->id.name, ncontrol->name, sizeof(kctl->id.name));
                if (strcmp(ncontrol->name, kctl->id.name) != 0)
                        pr_warn("ALSA: Control name '%s' truncated to '%s'\n",
                                ncontrol->name, kctl->id.name);
        }
        kctl->id.index = ncontrol->index;

        kctl->info = ncontrol->info;

        kctl->info = ncontrol->info;
        kctl->get = ncontrol->get;
        kctl->put = ncontrol->put;
        kctl->tlv.p = ncontrol->tlv.p;

        kctl->private_value = ncontrol->private_value;
        kctl->private_data = private_data;

        return kctl;
}

The snd_ctl_new function is used to allocate a kcontrol and return via kctl;

/**
 * snd_ctl_new - create a new control instance with some elements
 * @kctl: the pointer to store new control instance
 * @count: the number of elements in this control
 * @access: the default access flags for elements in this control
 * @file: given when locking these elements
 *
 * Allocates a memory object for a new control instance. The instance has
 * elements as many as the given number (@count). Each element has given
 * access permissions (@access). Each element is locked when @file is given.
 *
 * Return: 0 on success, error code on failure
 */
static int snd_ctl_new(struct snd_kcontrol **kctl, unsigned int count,
                       unsigned int access, struct snd_ctl_file *file)
{
        unsigned int idx;

        if (count == 0 || count > MAX_CONTROL_COUNT)
                return -EINVAL;

        *kctl = kzalloc(struct_size(*kctl, vd, count), GFP_KERNEL);  // Dynamically allocate memory, data structure type is struct snd_kcontrol, also allocate memory for member vd
        if (!*kctl)
                return -ENOMEM;

        for (idx = 0; idx < count; idx++) {  // Initialize member vd, struct snd_kcontrol_volatile array, length is count
                (*kctl)->vd[idx].access = access;   // Set access flag
                (*kctl)->vd[idx].owner = file;
        }
        (*kctl)->count = count;  // Set quantity

        return 0;
}
2.3.2 snd_ctl_add

Once the control is created, we can call the snd_ctl_add function to add the kcontrol to the sound card’s controls linked list, which is located in sound/soc/control.c;

/**
 * snd_ctl_add - add the control instance to the card
 * @card: the card instance
 * @kcontrol: the control instance to add
 *
 * Adds the control instance created via snd_ctl_new() or
 * snd_ctl_new1() to the given card. Assigns also an unique
 * numid used for fast search.
 *
 * It frees automatically the control which cannot be added.
 *
 * Return: Zero if successful, or a negative error code on failure.
 *
 */
int snd_ctl_add(struct snd_card *card, struct snd_kcontrol *kcontrol)
{
        return snd_ctl_add_replace(card, kcontrol, CTL_ADD_EXCLUSIVE);
}

The implementation of the snd_ctl_add_replace function is as follows:

static int snd_ctl_add_replace(struct snd_card *card,
                               struct snd_kcontrol *kcontrol,
                               enum snd_ctl_add_mode mode)  // CTL_ADD_EXCLUSIVE
{
        int err = -EINVAL;

        if (! kcontrol)
                return err;
        if (snd_BUG_ON(!card || !kcontrol->info))
                goto error;

        down_write(&card->controls_rwsem);   // Acquire read-write semaphore for concurrent operations on the controls linked list
        err = __snd_ctl_add_replace(card, kcontrol, mode);
        up_write(&card->controls_rwsem); // Release semaphore
        if (err < 0)
                goto error;
        return 0;

 error:
        snd_ctl_free_one(kcontrol);
        return err;
}

The __snd_ctl_add_replace function implementation is as follows:

/* add/replace a new kcontrol object; call with card->controls_rwsem locked */
static int __snd_ctl_add_replace(struct snd_card *card,
                                 struct snd_kcontrol *kcontrol,
                                 enum snd_ctl_add_mode mode)  // CTL_ADD_EXCLUSIVE
{
        struct snd_ctl_elem_id id;
        unsigned int idx;
        struct snd_kcontrol *old;
        int err;

        id = kcontrol->id;
        if (id.index > UINT_MAX - kcontrol->count)
                return -EINVAL;

        old = snd_ctl_find_id(card, &id);  // Find matching items in card->controls linked list based on id (actually the id of card->controls member)
        if (!old) {     // Not found
                if (mode == CTL_REPLACE)
                        return -EINVAL;
        } else {  // If it already exists, return an error message
                if (mode == CTL_ADD_EXCLUSIVE) {
                        dev_err(card->dev,
                                "control %i:%i:%i:%s:%i is already present\n",
                                id.iface, id.device, id.subdevice, id.name,
                                id.index);
                        return -EBUSY;
                }

                err = snd_ctl_remove(card, old);  // Remove
                if (err < 0)
                        return err;
        }

        if (snd_ctl_find_hole(card, kcontrol->count) < 0)
                return -ENOMEM;

        list_add_tail(&kcontrol->list, &card->controls); // Add kcontrl->list linked list node to card->controls linked list
        card->controls_count += kcontrol->count;   // Count
        kcontrol->id.numid = card->last_numid + 1;  // Allocate a number
        card->last_numid += kcontrol->count;

        add_hash_entries(card, kcontrol);

        for (idx = 0; idx < kcontrol->count; idx++)
                snd_ctl_notify_one(card, SNDRV_CTL_EVENT_MASK_ADD, kcontrol, idx);

        return 0;
}
2.3.3 snd_soc_add_component_controls

The ASoC CORE wraps snd_ctl_new1 and snd_ctl_add, providing the snd_soc_add_component_controls function to create and add multiple kcontrols to the sound card’s controls linked list based on the kcontrol template array, which is defined in sound/soc/soc-core.c:

/**
 * snd_soc_add_component_controls - Add an array of controls to a component.
 *
 * @component: Component to add controls to
 * @controls: Array of controls to add
 * @num_controls: Number of elements in the array
 *
 * Return: 0 for success, else error.
 */
int snd_soc_add_component_controls(struct snd_soc_component *component,
        const struct snd_kcontrol_new *controls, unsigned int num_controls)
{
        struct snd_card *card = component->card->snd_card;

        return snd_soc_add_controls(card, component->dev, controls,
                        num_controls, component->name_prefix, component);
} 

Specifically, this is accomplished through the snd_soc_add_controls function:

static int snd_soc_add_controls(struct snd_card *card, struct device *dev,
        const struct snd_kcontrol_new *controls, int num_controls,
        const char *prefix, void *data)
{
        int i;

        for (i = 0; i < num_controls; i++) {  // Register each snd_kcontrol_new
                const struct snd_kcontrol_new *control = &controls[i];
                int err = snd_ctl_add(card, snd_soc_cnew(control, data, // Here actually calls snd_soc_cnew to create snd_kcontrol instance
                                                         control->name, prefix));
                if (err < 0) {
                        dev_err(dev, "ASoC: Failed to add %s: %d\n",
                                control->name, err);
                        return err;
                }
        }

        return 0;
}

3. Control Device Files

After we migrate external audio drivers, we can see PCM device files in the /dev/snd directory. For example, after migrating the ALC5651 sound card driver on the NanoPC-T4 development board;

root@rk3399:~# ll /dev/snd
drwxr-xr-x  2 root root       60 Aug 24 21:31 by-path/
crw-rw----  1 root audio 116,  4 Aug 24 21:31 controlC0
crw-rw----  1 root audio 116,  3 Aug 24 21:31 pcmC0D0c
crw-rw----  1 root audio 116,  2 Aug 24 21:31 pcmC0D0p
crw-rw----  1 root audio 116,  1 Aug 24 21:31 seq
crw-rw----  1 root audio 116, 33 Aug 24 21:31 timer

We can see that these character device’s major device numbers are all 116, among which:

  • • controlC0: Used for sound card control, such as channel selection, mixing, microphone control, volume increase/decrease, switches, etc.;
  • • pcmC0D0c: Used for recording PCM devices;
  • • pcmC0D0p: Used for playback PCM devices;
  • • seq: Sequencer;
  • • timer: Timer;

C0D0 represents device 0 in sound card 0, where pcmC0D0c’s last c represents capture, and pcmC0D0p’s last p represents playback.

3.1 Opening Control Devices at the Application Layer

When we open Control devices at the application layer, such as dev/snd/controlC0. For character devices, when we perform an open operation, it actually executes the open function of the character device’s file operation set, which is the open function in snd_ctl_f_ops, that is snd_ctl_open;

/*
 *  INIT PART
 */
static const struct file_operations snd_ctl_f_ops =
{
        .owner =        THIS_MODULE,
        .read =         snd_ctl_read,
        .open =         snd_ctl_open,
        .release =      snd_ctl_release,
        .llseek =       no_llseek,
        .poll =         snd_ctl_poll,
        .unlocked_ioctl =   snd_ctl_ioctl,
        .compat_ioctl = snd_ctl_ioctl_compat,
        .fasync =       snd_ctl_fasync,
};

Here, let’s take a rough look at what the snd_ctl_open function does:

static int snd_ctl_open(struct inode *inode, struct file *file)
{
        unsigned long flags;
        struct snd_card *card;
        struct snd_ctl_file *ctl;
        int i, err;

        err = stream_open(inode, file);
        if (err < 0)
                return err;

        card = snd_lookup_minor_data(iminor(inode), SNDRV_DEVICE_TYPE_CONTROL); // Get sound card device
        if (!card) {
                err = -ENODEV;
                goto __error1;
        }
        err = snd_card_file_add(card, file); // Add file to card's file list files_list
        if (err < 0) {
                err = -ENODEV;
                goto __error1;
        }
        if (!try_module_get(card->module)) {
                err = -EFAULT;
                goto __error2;
        }
        ctl = kzalloc(sizeof(*ctl), GFP_KERNEL);   // Dynamically allocate memory, data structure type is struct snd_ctl_file
        if (ctl == NULL) {
                err = -ENOMEM;
                goto __error;
        }
        INIT_LIST_HEAD(&ctl->events);             // Initialize linked list node
        init_waitqueue_head(&ctl->change_sleep);  // Initialize wait queue head
        spin_lock_init(&ctl->read_lock);          // Initialize spin lock  
        ctl->card = card;                         // Set sound card device     
        for (i = 0; i < SND_CTL_SUBDEV_ITEMS; i++)
                ctl->preferred_subdevice[i] = -1;
        ctl->pid = get_pid(task_pid(current));    // Get process id
        file->private_data = ctl;
        write_lock_irqsave(&card->ctl_files_rwlock, flags);
        list_add_tail(&ctl->list, &card->ctl_files);   // Add ctl->list linked list node to card->ctl_files linked list
        write_unlock_irqrestore(&card->ctl_files_rwlock, flags);
        snd_card_unref(card);
        return 0;

      __error:
        module_put(card->module);
      __error2:
        snd_card_file_remove(card, file);
      __error1:
        if (card)
                snd_card_unref(card);
        return err;
}

References

[1] Exploration Journey of RK3399 / Quick Read of Audio Driver Layer

[2] Linux ALSA Audio Driver Part 1: Framework Overview

[3] Linux Audio Subsystem

[4] http://www.alsa-project.org/。

[5] Linux ALSA Driver Part 2: Sound Card Creation Process

[6] Linked Lists in the Linux Kernel – struct list_head

[7] Linux ALSA Audio System: Physical Link Part

[8] Sound Subsystem Documentation

[9] Detailed Explanation of DAPM in ALSA Sound Card Driver Part 1: kcontrol

Leave a Comment