blob: fe3e2d09cd46fba28dca688ea77433e9e3149a56 [file] [log] [blame]
/* Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <alsa/asoundlib.h>
#include <stdio.h>
#include <syslog.h>
#include "cras_alsa_mixer.h"
#include "cras_card_config.h"
#include "cras_util.h"
#include "cras_volume_curve.h"
#include "utlist.h"
/* Represents an ALSA control element. Each device can have several of these,
* each potentially having independent volume and mute controls.
* elem - ALSA mixer element.
* has_volume - non-zero indicates there is a volume control.
* has_mute - non-zero indicates there is a mute switch.
*/
struct mixer_control {
snd_mixer_elem_t *elem;
int has_volume;
int has_mute;
struct mixer_control *prev, *next;
};
/* Represents an ALSA control element related to a specific output such as
* speakers or headphones. A device can have several of these, each potentially
* having independent volume and mute controls.
* max_volume_dB - Maximum volume available in the volume control.
* min_volume_dB - Minimum volume available in the volume control.
* volume_curve - Curve for this output.
*/
struct mixer_output_control {
struct mixer_control base;
long max_volume_dB;
long min_volume_dB;
struct cras_volume_curve *volume_curve;
};
/* Holds a reference to the opened mixer and the volume controls.
* mixer - Pointer to the opened alsa mixer.
* main_volume_controls - List of volume controls (normally 'Master' and 'PCM').
* playback_switch - Switch used to mute the device.
* main_capture_controls - List of capture gain controls (normally 'Capture').
* capture_switch - Switch used to mute the capture stream.
* volume_curve - Default volume curve that converts from an index to dBFS.
* max_volume_dB - Maximum volume available in main volume controls. The dBFS
* value setting will be applied relative to this.
* min_volume_dB - Minimum volume available in main volume controls.
* config - Config info for this card, can be NULL if none found.
*/
struct cras_alsa_mixer {
snd_mixer_t *mixer;
struct mixer_control *main_volume_controls;
struct mixer_control *output_controls;
snd_mixer_elem_t *playback_switch;
struct mixer_control *main_capture_controls;
struct mixer_control *input_controls;
snd_mixer_elem_t *capture_switch;
struct cras_volume_curve *volume_curve;
long max_volume_dB;
long min_volume_dB;
const struct cras_card_config *config;
};
/* Wrapper for snd_mixer_open and helpers.
* Args:
* mixdev - Name of the device to open the mixer for.
* Returns:
* pointer to opened mixer on success, NULL on failure.
*/
static snd_mixer_t *alsa_mixer_open(const char *mixdev)
{
snd_mixer_t *mixer = NULL;
int rc;
rc = snd_mixer_open(&mixer, 0);
if (rc < 0)
return NULL;
rc = snd_mixer_attach(mixer, mixdev);
if (rc < 0)
goto fail_after_open;
rc = snd_mixer_selem_register(mixer, NULL, NULL);
if (rc < 0)
goto fail_after_open;
rc = snd_mixer_load(mixer);
if (rc < 0)
goto fail_after_open;
return mixer;
fail_after_open:
snd_mixer_close(mixer);
return NULL;
}
/* Checks if the given element's name is in the list. */
static int name_in_list(const char *name,
const char * const list[],
size_t len)
{
size_t i;
for (i = 0; i < len; i++)
if (list[i] && strcmp(list[i], name) == 0)
return 1;
return 0;
}
/* Adds the main volume control to the list and grabs the first seen playback
* switch to use for mute. */
static int add_main_volume_control(struct cras_alsa_mixer *cmix,
snd_mixer_elem_t *elem)
{
if (snd_mixer_selem_has_playback_volume(elem)) {
struct mixer_control *c, *next;
long min, max;
c = (struct mixer_control *)calloc(1, sizeof(*c));
if (c == NULL) {
syslog(LOG_ERR, "No memory for mixer.");
return -ENOMEM;
}
c->elem = elem;
if (snd_mixer_selem_get_playback_dB_range(elem,
&min,
&max) == 0) {
cmix->max_volume_dB += max;
cmix->min_volume_dB += min;
}
DL_FOREACH(cmix->main_volume_controls, next) {
long next_min, next_max;
snd_mixer_selem_get_playback_dB_range(next->elem,
&next_min,
&next_max);
if (max - min > next_max - next_min)
break;
}
DL_INSERT(cmix->main_volume_controls, next, c);
}
/* If cmix doesn't yet have a playback switch and this is a playback
* switch, use it. */
if (cmix->playback_switch == NULL &&
snd_mixer_selem_has_playback_switch(elem))
cmix->playback_switch = elem;
return 0;
}
/* Adds the main capture control to the list and grabs the first seen capture
* switch to mute input. */
static int add_main_capture_control(struct cras_alsa_mixer *cmix,
snd_mixer_elem_t *elem)
{
/* TODO(dgreid) handle index != 0, map to correct input. */
if (snd_mixer_selem_get_index(elem) > 0)
return 0;
if (snd_mixer_selem_has_capture_volume(elem)) {
struct mixer_control *c;
c = (struct mixer_control *)calloc(1, sizeof(*c));
if (c == NULL) {
syslog(LOG_ERR, "No memory for control.");
return -ENOMEM;
}
c->elem = elem;
syslog(LOG_DEBUG,
"Add capture control %s\n",
snd_mixer_selem_get_name(elem));
DL_APPEND(cmix->main_capture_controls, c);
}
/* If cmix doesn't yet have a capture switch and this is a capture
* switch, use it. */
if (cmix->capture_switch == NULL &&
snd_mixer_selem_has_capture_switch(elem))
cmix->capture_switch = elem;
return 0;
}
/* Creates a volume curve for a new output. */
static struct cras_volume_curve *create_volume_curve_for_output(
const struct cras_alsa_mixer *cmix,
snd_mixer_elem_t *elem)
{
const char *output_name;
output_name = snd_mixer_selem_get_name(elem);
return cras_card_config_get_volume_curve_for_control(cmix->config,
output_name);
}
/* Adds an output control to the list. */
static int add_output_control(struct cras_alsa_mixer *cmix,
snd_mixer_elem_t *elem)
{
int index; /* Index part of mixer simple element */
struct mixer_control *c;
struct mixer_output_control *output;
long min, max;
index = snd_mixer_selem_get_index(elem);
syslog(LOG_DEBUG, "Add output control: %s,%d\n",
snd_mixer_selem_get_name(elem), index);
output = (struct mixer_output_control *)calloc(1, sizeof(*output));
if (output == NULL) {
syslog(LOG_ERR, "No memory for output control.");
return -ENOMEM;
}
if (snd_mixer_selem_get_playback_dB_range(elem, &min, &max) == 0) {
output->max_volume_dB = max;
output->min_volume_dB = min;
}
output->volume_curve = create_volume_curve_for_output(cmix, elem);
c = &output->base;
c->elem = elem;
c->has_volume = snd_mixer_selem_has_playback_volume(elem);
c->has_mute = snd_mixer_selem_has_playback_switch(elem);
DL_APPEND(cmix->output_controls, c);
return 0;
}
/* Adds an input control to the list. */
static int add_input_control(struct cras_alsa_mixer *cmix,
snd_mixer_elem_t *elem)
{
int index; /* Index part of mixer simple element */
struct mixer_control *c;
index = snd_mixer_selem_get_index(elem);
syslog(LOG_DEBUG, "Add input control: %s,%d\n",
snd_mixer_selem_get_name(elem), index);
c = (struct mixer_control *)calloc(1, sizeof(*c));
if (c == NULL) {
syslog(LOG_ERR, "No memory for input control.");
return -ENOMEM;
}
c->elem = elem;
c->has_volume = snd_mixer_selem_has_capture_volume(elem);
c->has_mute = snd_mixer_selem_has_capture_switch(elem);
DL_APPEND(cmix->input_controls, c);
return 0;
}
static void list_controls(struct mixer_control *control_list,
cras_alsa_mixer_control_callback cb,
void *cb_arg)
{
struct mixer_control *control;
DL_FOREACH(control_list, control)
cb(control, cb_arg);
}
static struct mixer_control *get_control_matching_name(
struct mixer_control *control_list,
const char *name)
{
struct mixer_control *c;
DL_FOREACH(control_list, c) {
const char *elem_name;
elem_name = snd_mixer_selem_get_name(c->elem);
if (elem_name == NULL)
continue;
if (strstr(name, elem_name))
return c;
}
return NULL;
}
/*
* Exported interface.
*/
struct cras_alsa_mixer *cras_alsa_mixer_create(
const char *card_name,
const struct cras_card_config *config,
const char *output_names_extra[],
size_t output_names_extra_size,
const char *extra_main_volume)
{
/* Names of controls for main system volume. */
static const char * const main_volume_names[] = {
"Master",
"Digital",
"PCM",
};
/* Names of controls for individual outputs. */
static const char * const output_names[] = {
"Headphone",
"Headset",
"HDMI",
"Speaker",
};
/* Names of controls for capture gain/attenuation and mute. */
static const char * const main_capture_names[] = {
"Capture",
"Digital Capture",
};
/* Names of controls for individual inputs. */
static const char * const input_names[] = {
"Mic",
"Microphone",
};
snd_mixer_elem_t *elem;
struct cras_alsa_mixer *cmix;
snd_mixer_elem_t *other_elem = NULL;
long other_dB_range = 0;
cmix = (struct cras_alsa_mixer *)calloc(1, sizeof(*cmix));
if (cmix == NULL)
return NULL;
syslog(LOG_DEBUG, "Add mixer for device %s", card_name);
cmix->mixer = alsa_mixer_open(card_name);
if (cmix->mixer == NULL) {
syslog(LOG_DEBUG, "Couldn't open mixer.");
free(cmix);
return NULL;
}
cmix->config = config;
cmix->volume_curve =
cras_card_config_get_volume_curve_for_control(cmix->config,
"Default");
/* Find volume and mute controls. */
for(elem = snd_mixer_first_elem(cmix->mixer);
elem != NULL; elem = snd_mixer_elem_next(elem)) {
const char *name;
name = snd_mixer_selem_get_name(elem);
if (name == NULL)
continue;
if (!extra_main_volume &&
name_in_list(name, main_volume_names,
ARRAY_SIZE(main_volume_names))) {
if (add_main_volume_control(cmix, elem) != 0) {
cras_alsa_mixer_destroy(cmix);
return NULL;
}
} else if (name_in_list(name, main_capture_names,
ARRAY_SIZE(main_capture_names))) {
if (add_main_capture_control(cmix, elem) != 0) {
cras_alsa_mixer_destroy(cmix);
return NULL;
}
} else if (name_in_list(name, output_names,
ARRAY_SIZE(output_names))
|| name_in_list(name, output_names_extra,
output_names_extra_size)) {
/* TODO(dgreid) - determine device index. */
if (add_output_control(cmix, elem) != 0) {
cras_alsa_mixer_destroy(cmix);
return NULL;
}
} else if (name_in_list(name, input_names,
ARRAY_SIZE(input_names))) {
if (add_input_control(cmix, elem) != 0) {
cras_alsa_mixer_destroy(cmix);
return NULL;
}
} else if (extra_main_volume &&
!strcmp(name, extra_main_volume)) {
if (add_main_volume_control(cmix, elem) != 0) {
cras_alsa_mixer_destroy(cmix);
return NULL;
}
} else if (snd_mixer_selem_has_playback_volume(elem)) {
/* Temporarily cache one elem whose name is not
* in the list above, but has a playback volume
* control and the largest volume range. */
long min, max, range;
if (snd_mixer_selem_get_playback_dB_range(elem,
&min,
&max) != 0)
continue;
range = max - min;
if (other_dB_range < range) {
other_dB_range = range;
other_elem = elem;
}
}
}
/* If there is no volume control and output control found,
* use the volume control which has the largest volume range
* in the mixer as a main volume control. */
if (!cmix->main_volume_controls && !cmix->output_controls &&
other_elem) {
if (add_main_volume_control(cmix, other_elem) != 0) {
cras_alsa_mixer_destroy(cmix);
return NULL;
}
}
return cmix;
}
void cras_alsa_mixer_destroy(struct cras_alsa_mixer *cras_mixer)
{
struct mixer_control *c;
assert(cras_mixer);
DL_FOREACH(cras_mixer->main_volume_controls, c) {
DL_DELETE(cras_mixer->main_volume_controls, c);
free(c);
}
DL_FOREACH(cras_mixer->main_capture_controls, c) {
DL_DELETE(cras_mixer->main_capture_controls, c);
free(c);
}
DL_FOREACH(cras_mixer->output_controls, c) {
struct mixer_output_control *output;
output = (struct mixer_output_control *)c;
cras_volume_curve_destroy(output->volume_curve);
DL_DELETE(cras_mixer->output_controls, c);
free(output);
}
DL_FOREACH(cras_mixer->input_controls, c) {
DL_DELETE(cras_mixer->input_controls, c);
free(c);
}
cras_volume_curve_destroy(cras_mixer->volume_curve);
snd_mixer_close(cras_mixer->mixer);
free(cras_mixer);
}
const struct cras_volume_curve *cras_alsa_mixer_default_volume_curve(
const struct cras_alsa_mixer *cras_mixer)
{
assert(cras_mixer);
assert(cras_mixer->volume_curve);
return cras_mixer->volume_curve;
}
void cras_alsa_mixer_set_dBFS(struct cras_alsa_mixer *cras_mixer,
long dBFS,
struct mixer_control *mixer_output)
{
struct mixer_control *c;
struct mixer_output_control *output;
output = (struct mixer_output_control *)mixer_output;
long to_set;
assert(cras_mixer);
/* dBFS is normally < 0 to specify the attenuation from max. max is the
* combined max of the master controls and the current output.
*/
to_set = dBFS + cras_mixer->max_volume_dB;
if (mixer_output)
to_set += output->max_volume_dB;
/* Go through all the controls, set the volume level for each,
* taking the value closest but greater than the desired volume. If the
* entire volume can't be set on the current control, move on to the
* next one until we have the exact volume, or gotten as close as we
* can. Once all of the volume is set the rest of the controls should be
* set to 0dB. */
DL_FOREACH(cras_mixer->main_volume_controls, c) {
long actual_dB;
snd_mixer_selem_set_playback_dB_all(c->elem, to_set, 1);
snd_mixer_selem_get_playback_dB(c->elem,
SND_MIXER_SCHN_FRONT_LEFT,
&actual_dB);
to_set -= actual_dB;
}
/* Apply the rest to the output-specific control. */
if (mixer_output && mixer_output->elem && mixer_output->has_volume)
snd_mixer_selem_set_playback_dB_all(mixer_output->elem,
to_set,
1);
}
long cras_alsa_mixer_get_dB_range(struct cras_alsa_mixer *cras_mixer)
{
if (!cras_mixer)
return 0;
return cras_mixer->max_volume_dB - cras_mixer->min_volume_dB;
}
long cras_alsa_mixer_get_output_dB_range(
struct mixer_control *mixer_output)
{
struct mixer_output_control *output;
if (!mixer_output || !mixer_output->elem || !mixer_output->has_volume)
return 0;
output = (struct mixer_output_control *)mixer_output;
return output->max_volume_dB - output->min_volume_dB;
}
void cras_alsa_mixer_set_capture_dBFS(struct cras_alsa_mixer *cras_mixer,
long dBFS,
struct mixer_control *mixer_input)
{
struct mixer_control *c;
long to_set;
assert(cras_mixer);
to_set = dBFS;
/* Go through all the controls, set the gain for each, taking the value
* closest but greater than the desired gain. If the entire gain can't
* be set on the current control, move on to the next one until we have
* the exact gain, or gotten as close as we can. Once all of the gain is
* set the rest of the controls should be set to 0dB. */
DL_FOREACH(cras_mixer->main_capture_controls, c) {
long actual_dB;
snd_mixer_selem_set_capture_dB_all(c->elem, to_set, 1);
snd_mixer_selem_get_capture_dB(c->elem,
SND_MIXER_SCHN_FRONT_LEFT,
&actual_dB);
to_set -= actual_dB;
}
/* Apply the reset to input specific control */
if (mixer_input && mixer_input->elem && mixer_input->has_volume)
snd_mixer_selem_set_capture_dB_all(mixer_input->elem,
to_set, 1);
assert(cras_mixer);
}
long cras_alsa_mixer_get_minimum_capture_gain(
struct cras_alsa_mixer *cmix,
struct mixer_control *mixer_input)
{
struct mixer_control *c;
long min, max, total_min;
assert(cmix);
total_min = 0;
DL_FOREACH(cmix->main_capture_controls, c)
if (snd_mixer_selem_get_capture_dB_range(c->elem,
&min, &max) == 0)
total_min += min;
if (mixer_input && snd_mixer_selem_get_capture_dB_range(
mixer_input->elem, &min, &max) == 0)
total_min += min;
return total_min;
}
long cras_alsa_mixer_get_maximum_capture_gain(struct cras_alsa_mixer *cmix,
struct mixer_control *mixer_input)
{
struct mixer_control *c;
long min, max, total_max;
assert(cmix);
total_max = 0;
DL_FOREACH(cmix->main_capture_controls, c)
if (snd_mixer_selem_get_capture_dB_range(c->elem,
&min, &max) == 0)
total_max += max;
if (mixer_input && snd_mixer_selem_get_capture_dB_range(
mixer_input->elem, &min, &max) == 0)
total_max += max;
return total_max;
}
void cras_alsa_mixer_set_mute(struct cras_alsa_mixer *cras_mixer,
int muted,
struct mixer_control *mixer_output)
{
assert(cras_mixer);
if (cras_mixer->playback_switch) {
snd_mixer_selem_set_playback_switch_all(
cras_mixer->playback_switch, !muted);
return;
}
if (mixer_output && mixer_output->has_mute)
snd_mixer_selem_set_playback_switch_all(
mixer_output->elem, !muted);
}
void cras_alsa_mixer_set_capture_mute(struct cras_alsa_mixer *cras_mixer,
int muted,
struct mixer_control *mixer_input)
{
assert(cras_mixer);
if (cras_mixer->capture_switch) {
snd_mixer_selem_set_capture_switch_all(
cras_mixer->capture_switch, !muted);
return;
}
if (mixer_input && mixer_input->has_mute)
snd_mixer_selem_set_capture_switch_all(
mixer_input->elem, !muted);
}
void cras_alsa_mixer_list_outputs(struct cras_alsa_mixer *cras_mixer,
cras_alsa_mixer_control_callback cb,
void *cb_arg)
{
assert(cras_mixer);
list_controls(cras_mixer->output_controls, cb, cb_arg);
}
void cras_alsa_mixer_list_inputs(struct cras_alsa_mixer *cras_mixer,
cras_alsa_mixer_control_callback cb,
void *cb_arg)
{
assert(cras_mixer);
list_controls(cras_mixer->input_controls, cb, cb_arg);
}
const char *cras_alsa_mixer_get_control_name(
const struct mixer_control *control)
{
return snd_mixer_selem_get_name(control->elem);
}
struct mixer_control *cras_alsa_mixer_get_output_matching_name(
const struct cras_alsa_mixer *cras_mixer,
const char * const name)
{
assert(cras_mixer);
return get_control_matching_name(cras_mixer->output_controls, name);
}
struct mixer_control *cras_alsa_mixer_get_input_matching_name(
struct cras_alsa_mixer *cras_mixer,
const char *name)
{
struct mixer_control *c = NULL;
snd_mixer_elem_t *elem;
assert(cras_mixer);
c = get_control_matching_name(cras_mixer->input_controls, name);
if (c)
return c;
/* TODO: This is a workaround, we should pass the input names in
* ucm config to cras_alsa_mixer_create. */
for (elem = snd_mixer_first_elem(cras_mixer->mixer);
elem != NULL; elem = snd_mixer_elem_next(elem)) {
const char *control_name;
control_name = snd_mixer_selem_get_name(elem);
if (control_name == NULL)
continue;
if (strcmp(name, control_name) == 0) {
if (add_input_control(cras_mixer, elem) == 0)
return cras_mixer->input_controls->prev;
}
}
return NULL;
}
int cras_alsa_mixer_set_output_active_state(
struct mixer_control *output,
int active)
{
assert(output);
if (!output->has_mute)
return -1;
return snd_mixer_selem_set_playback_switch_all(output->elem, active);
}
struct cras_volume_curve *cras_alsa_mixer_create_volume_curve_for_name(
const struct cras_alsa_mixer *cmix,
const char *name)
{
if (cmix != NULL)
return cras_card_config_get_volume_curve_for_control(
cmix->config, name);
else
return cras_card_config_get_volume_curve_for_control(NULL,
name);
}
struct cras_volume_curve *cras_alsa_mixer_get_output_volume_curve(
const struct mixer_control *control)
{
struct mixer_output_control *output;
output = (struct mixer_output_control *)control;
if (output)
return output->volume_curve;
else
return NULL;
}