blob: 6d4d7bf55da4c5bfbf24ee16c1c7ee81cd655026 [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 <linux/input.h>
#include <regex.h>
#include <syslog.h>
#include "cras_alsa_jack.h"
#include "cras_alsa_mixer.h"
#include "cras_alsa_ucm.h"
#include "cras_system_state.h"
#include "cras_gpio_jack.h"
#include "cras_tm.h"
#include "cras_util.h"
#include "edid_utils.h"
#include "utlist.h"
static const unsigned int DISPLAY_INFO_RETRY_DELAY_MS = 200;
static const unsigned int DISPLAY_INFO_MAX_RETRIES = 10;
static const unsigned int DISPLAY_INFO_GPIO_MAX_RETRIES = 25;
/* Constants used to retrieve monitor name from ELD buffer. */
static const unsigned int ELD_MNL_MASK = 31;
static const unsigned int ELD_MNL_OFFSET = 4;
static const unsigned int ELD_MONITOR_NAME_OFFSET = 20;
/* Keeps an fd that is registered with system settings. A list of fds must be
* kept so that they can be removed when the jack list is destroyed. */
struct jack_poll_fd {
int fd;
struct jack_poll_fd *prev, *next;
};
/* cras_gpio_jack: Describes headphone & microphone jack connected to GPIO
*
* On Arm-based systems, the headphone & microphone jacks are
* connected to GPIOs which are plumbed through the /dev/input/event
* system. For these jacks, the software is written to open the
* corresponding /dev/input/event file and monitor it for 'insert' &
* 'remove' activity.
*
* fd : File descriptor corresponding to the /dev/input/event file.
*
* switch_event : Indicates the type of the /dev/input/event file.
* Either SW_HEADPHONE_INSERT, or SW_MICROPHONE_INSERT.
*
* current_state: 0 -> device not plugged in
* 1 -> device plugged in
* device_name : Device name extracted from /dev/input/event[0..9]+.
* Allocated on heap; must free.
*/
struct cras_gpio_jack {
int fd;
unsigned switch_event;
unsigned current_state;
char *device_name;
};
/* Represents a single alsa Jack, e.g. "Headphone Jack" or "Mic Jack".
* is_gpio: 1 -> gpio switch (union field: gpio)
* 0 -> Alsa 'jack' (union field: elem)
* elem - alsa hcontrol element for this jack, when is_gpio == 0.
* gpio - description of gpio-based jack, when is_gpio != 0.
* eld_control - mixer control for ELD info buffer.
* jack_list - list of jacks this belongs to.
* mixer_output - mixer output control used to control audio to this jack.
* This will be null for input jacks.
* mixer_input - mixer input control used to control audio to this jack.
* This will be null for output jacks.
* ucm_device - Name of the ucm device if found, otherwise, NULL.
* edid_file - File to read the EDID from (if available, HDMI only).
* display_info_timer - Timer used to poll display info for HDMI jacks.
* display_info_retries - Number of times to retry reading display info.
*
* mixer_output/mixer_input fields are only used to find the node for this
* jack. These are not used for setting volume or mute. There should be a
* 1:1 map between node and jack. node->jack follows the pointer; jack->node
* is done by either searching node->jack pointers or searching the node that
* has the same mixer_control as the jack.
*/
struct cras_alsa_jack {
unsigned is_gpio; /* !0 -> 'gpio' valid
* 0 -> 'elem' valid
*/
union {
snd_hctl_elem_t *elem;
struct cras_gpio_jack gpio;
};
snd_hctl_elem_t *eld_control;
struct cras_alsa_jack_list *jack_list;
struct mixer_control *mixer_output;
struct mixer_control *mixer_input;
char *ucm_device;
const char *override_type_name;
const char *edid_file;
struct cras_timer *display_info_timer;
unsigned int display_info_retries;
struct cras_alsa_jack *prev, *next;
};
/* Contains all Jacks for a given device.
* hctl - alsa hcontrol for this device's card
* - not opened by the jack list.
* mixer - cras mixer for the card providing this device.
* ucm - CRAS use case manager if available.
* card_index - Index ALSA uses to refer to the card. The X in "hw:X".
* card_name - The name of the card.
* device_index - Index ALSA uses to refer to the device. The Y in "hw:X,Y".
* is_first_device - whether this device is the first device on the card.
* direction - Input or output.
* change_callback - function to call when the state of a jack changes.
* callback_data - data to pass back to the callback.
* jacks - list of jacks for this device.
*/
struct cras_alsa_jack_list {
snd_hctl_t *hctl;
struct cras_alsa_mixer *mixer;
struct cras_use_case_mgr *ucm;
unsigned int card_index;
const char *card_name;
size_t device_index;
int is_first_device;
enum CRAS_STREAM_DIRECTION direction;
jack_state_change_callback *change_callback;
void *callback_data;
struct cras_alsa_jack *jacks;
};
/* Used to contain information needed while looking through GPIO jacks.
* jack_list - The current jack_list.
* section - An associated UCM section.
* result_jack - The resulting jack.
* rc - The return code for the operation.
*/
struct gpio_switch_list_data {
struct cras_alsa_jack_list *jack_list;
struct ucm_section *section;
struct cras_alsa_jack *result_jack;
int rc;
};
/*
* Local Helpers.
*/
#define BITS_PER_BYTE (8)
#define BITS_PER_LONG (sizeof(long) * BITS_PER_BYTE)
#define NBITS(x) ((((x)-1) / BITS_PER_LONG) + 1)
#define OFF(x) ((x) % BITS_PER_LONG)
#define BIT(x) (1UL << OFF(x))
#define LONG(x) ((x) / BITS_PER_LONG)
#define IS_BIT_SET(bit, array) !!((array[LONG(bit)]) & (1UL << OFF(bit)))
static int sys_input_get_switch_state(int fd, unsigned sw, unsigned *state)
{
unsigned long bits[NBITS(SW_CNT)];
const unsigned long switch_no = sw;
memset(bits, '\0', sizeof(bits));
/* If switch event present & supported, get current state. */
if (gpio_switch_eviocgbit(fd, bits, sizeof(bits)) < 0)
return -EIO;
if (IS_BIT_SET(switch_no, bits))
if (gpio_switch_eviocgsw(fd, bits, sizeof(bits)) >= 0) {
*state = IS_BIT_SET(switch_no, bits);
return 0;
}
return -1;
}
static inline struct cras_alsa_jack *cras_alloc_jack(int is_gpio)
{
struct cras_alsa_jack *jack = calloc(1, sizeof(*jack));
if (jack == NULL)
return NULL;
jack->is_gpio = is_gpio;
return jack;
}
static void cras_free_jack(struct cras_alsa_jack *jack, int rm_select_fd)
{
if (!jack)
return;
free(jack->ucm_device);
free((void *)jack->edid_file);
if (jack->display_info_timer)
cras_tm_cancel_timer(cras_system_state_get_tm(),
jack->display_info_timer);
if (jack->is_gpio) {
free(jack->gpio.device_name);
if (jack->gpio.fd >= 0) {
if (rm_select_fd)
cras_system_rm_select_fd(jack->gpio.fd);
close(jack->gpio.fd);
}
}
/*
* Remove the jack callback set on hctl. Otherwise, snd_hctl_close will
* trigger a callback while iodev might already be destroyed.
*/
if (!jack->is_gpio && jack->elem)
snd_hctl_elem_set_callback(jack->elem, NULL);
free((void *)jack->override_type_name);
free(jack);
}
/* Gets the current plug state of the jack */
static int get_jack_current_state(struct cras_alsa_jack *jack)
{
snd_ctl_elem_value_t *elem_value;
if (jack->is_gpio)
return jack->gpio.current_state;
snd_ctl_elem_value_alloca(&elem_value);
snd_hctl_elem_read(jack->elem, elem_value);
return snd_ctl_elem_value_get_boolean(elem_value, 0);
}
static int read_jack_edid(const struct cras_alsa_jack *jack, uint8_t *edid)
{
int fd, nread;
fd = open(jack->edid_file, O_RDONLY);
if (fd < 0)
return -1;
nread = read(fd, edid, EEDID_SIZE);
close(fd);
if (nread < EDID_SIZE || !edid_valid(edid))
return -1;
return 0;
}
static int check_jack_edid(struct cras_alsa_jack *jack)
{
uint8_t edid[EEDID_SIZE];
if (read_jack_edid(jack, edid))
return -1;
/* If the jack supports EDID, check that it supports audio, clearing
* the plugged state if it doesn't.
*/
if (!edid_lpcm_support(edid, edid[EDID_EXT_FLAG]))
jack->gpio.current_state = 0;
return 0;
}
static int get_jack_edid_monitor_name(const struct cras_alsa_jack *jack,
char *buf, unsigned int buf_size)
{
uint8_t edid[EEDID_SIZE];
if (read_jack_edid(jack, edid))
return -1;
return edid_get_monitor_name(edid, buf, buf_size);
}
/* Checks the ELD control of the jack to see if the ELD buffer
* is ready to read and report the plug status.
*/
static int check_jack_eld(struct cras_alsa_jack *jack)
{
snd_ctl_elem_info_t *elem_info;
snd_ctl_elem_info_alloca(&elem_info);
/* Poll ELD control by getting the count of ELD buffer.
* When seeing zero buffer count, retry after a delay until
* it's ready or reached the max number of retries. */
if (snd_hctl_elem_info(jack->eld_control, elem_info) != 0)
return -1;
if (snd_ctl_elem_info_get_count(elem_info) == 0)
return -1;
return 0;
}
static void display_info_delay_cb(struct cras_timer *timer, void *arg);
/* Callback function doing following things:
* 1. Reset timer and update max number of retries.
* 2. Check all conditions to see if it's okay or needed to
* report jack status directly. E.g. jack is unplugged or
* EDID is not ready for some reason.
* 3. Check if max number of retries is reached and decide
* to set timer for next callback or report jack state.
*/
static inline void jack_state_change_cb(struct cras_alsa_jack *jack, int retry)
{
struct cras_tm *tm = cras_system_state_get_tm();
if (jack->display_info_timer) {
cras_tm_cancel_timer(tm, jack->display_info_timer);
jack->display_info_timer = NULL;
}
if (retry) {
jack->display_info_retries =
jack->is_gpio ? DISPLAY_INFO_GPIO_MAX_RETRIES :
DISPLAY_INFO_MAX_RETRIES;
}
if (!get_jack_current_state(jack))
goto report_jack_state;
/* If there is an edid file, check it. If it is ready continue, if we
* need to try again later, return here as the timer has been armed and
* will check again later.
*/
if (jack->edid_file == NULL && jack->eld_control == NULL)
goto report_jack_state;
if (jack->edid_file && (check_jack_edid(jack) == 0))
goto report_jack_state;
if (jack->eld_control && (check_jack_eld(jack) == 0))
goto report_jack_state;
if (--jack->display_info_retries == 0) {
if (jack->is_gpio)
jack->gpio.current_state = 0;
if (jack->edid_file)
syslog(LOG_ERR, "Timeout to read EDID from %s",
jack->edid_file);
goto report_jack_state;
}
jack->display_info_timer = cras_tm_create_timer(
tm, DISPLAY_INFO_RETRY_DELAY_MS, display_info_delay_cb, jack);
return;
report_jack_state:
jack->jack_list->change_callback(jack, get_jack_current_state(jack),
jack->jack_list->callback_data);
}
/* gpio_switch_initial_state
*
* Determines the initial state of a gpio-based switch.
*/
static void gpio_switch_initial_state(struct cras_alsa_jack *jack)
{
unsigned v;
int r = sys_input_get_switch_state(jack->gpio.fd,
jack->gpio.switch_event, &v);
jack->gpio.current_state = r == 0 ? v : 0;
jack_state_change_cb(jack, 1);
}
/* Check if the input event is an audio switch event. */
static inline int is_audio_switch_event(const struct input_event *ev,
int sw_code)
{
return (ev->type == EV_SW && ev->code == sw_code);
}
/* Timer callback to read display info after a hotplug event for an HDMI jack.
*/
static void display_info_delay_cb(struct cras_timer *timer, void *arg)
{
struct cras_alsa_jack *jack = (struct cras_alsa_jack *)arg;
jack->display_info_timer = NULL;
jack_state_change_cb(jack, 0);
}
/* gpio_switch_callback
*
* This callback is invoked whenever the associated /dev/input/event
* file has data to read. Perform autoswitching to / from the
* associated device when data is available.
*/
static void gpio_switch_callback(void *arg, int events)
{
struct cras_alsa_jack *jack = arg;
int i;
int r;
struct input_event ev[64];
r = gpio_switch_read(jack->gpio.fd, ev,
ARRAY_SIZE(ev) * sizeof(struct input_event));
if (r < 0)
return;
for (i = 0; i < r / sizeof(struct input_event); ++i)
if (is_audio_switch_event(&ev[i], jack->gpio.switch_event)) {
jack->gpio.current_state = ev[i].value;
jack_state_change_cb(jack, 1);
}
}
/* Determines if the GPIO jack should be associated with the device of the
* jack list. If the device name is not specified in UCM (common case),
* assume it should be associated with the first input device or the first
* output device on the card.
*/
static unsigned int
gpio_jack_match_device(const struct cras_alsa_jack *jack,
struct cras_alsa_jack_list *jack_list,
enum CRAS_STREAM_DIRECTION direction)
{
int target_dev_idx;
/* If the device name is not specified in UCM, assume it should be
* associated with device 0. */
if (!jack_list->ucm || !jack->ucm_device)
return jack_list->is_first_device;
/* If jack has valid ucm_device, that means this jack has already been
* associated to this card. Next step to match device index on this
* card. */
target_dev_idx = ucm_get_alsa_dev_idx_for_dev(
jack_list->ucm, jack->ucm_device, direction);
if (target_dev_idx < 0)
return jack_list->is_first_device;
syslog(LOG_DEBUG,
"Matching GPIO jack, target device idx: %d, "
"current card name: %s, device index: %zu\n",
target_dev_idx, jack_list->card_name, jack_list->device_index);
return (target_dev_idx == jack_list->device_index);
}
static int create_jack_for_gpio(struct cras_alsa_jack_list *jack_list,
const char *pathname, const char *dev_name,
unsigned switch_event,
struct cras_alsa_jack **out_jack)
{
struct cras_alsa_jack *jack;
unsigned long bits[NBITS(SW_CNT)];
const char *card_name = jack_list->card_name;
int r;
if (!out_jack)
return -EINVAL;
*out_jack = NULL;
jack = cras_alloc_jack(1);
if (jack == NULL)
return -ENOMEM;
jack->gpio.fd = gpio_switch_open(pathname);
if (jack->gpio.fd == -1) {
r = -EIO;
goto error;
}
jack->gpio.switch_event = switch_event;
jack->jack_list = jack_list;
jack->gpio.device_name = strdup(dev_name);
if (!jack->gpio.device_name) {
r = -ENOMEM;
goto error;
}
if (!strstr(jack->gpio.device_name, card_name) ||
(gpio_switch_eviocgbit(jack->gpio.fd, bits, sizeof(bits)) < 0) ||
!IS_BIT_SET(switch_event, bits)) {
r = -EIO;
goto error;
}
*out_jack = jack;
return 0;
error:
/* Not yet registered with system select. */
cras_free_jack(jack, 0);
return r;
}
/* Take ownership and finish setup of the jack.
* Add the jack to the jack_list if everything goes well, or destroy it.
*/
static int cras_complete_gpio_jack(struct gpio_switch_list_data *data,
struct cras_alsa_jack *jack,
unsigned switch_event)
{
struct cras_alsa_jack_list *jack_list = data->jack_list;
int r;
if (jack->ucm_device) {
jack->edid_file = ucm_get_edid_file_for_dev(jack_list->ucm,
jack->ucm_device);
}
r = sys_input_get_switch_state(jack->gpio.fd, switch_event,
&jack->gpio.current_state);
if (r < 0) {
cras_free_jack(jack, 0);
return -EIO;
}
r = cras_system_add_select_fd(jack->gpio.fd, gpio_switch_callback, jack,
POLLIN);
if (r < 0) {
/* Not yet registered with system select. */
cras_free_jack(jack, 0);
return r;
}
DL_APPEND(jack_list->jacks, jack);
if (!data->result_jack)
data->result_jack = jack;
else if (data->section)
syslog(LOG_ERR, "More than one jack for SectionDevice '%s'.",
data->section->name);
return 0;
}
/* open_and_monitor_gpio:
*
* Opens a /dev/input/event file associated with a headphone /
* microphone jack and watches it for activity.
* Returns 0 when a jack has been successfully added.
*/
static int open_and_monitor_gpio(struct gpio_switch_list_data *data,
const char *pathname, const char *dev_name,
unsigned switch_event)
{
struct cras_alsa_jack *jack;
struct cras_alsa_jack_list *jack_list = data->jack_list;
enum CRAS_STREAM_DIRECTION direction = jack_list->direction;
int r;
r = create_jack_for_gpio(jack_list, pathname, dev_name, switch_event,
&jack);
if (r != 0)
return r;
if (jack_list->ucm)
jack->ucm_device = ucm_get_dev_for_jack(
jack_list->ucm, jack->gpio.device_name, direction);
if (!gpio_jack_match_device(jack, jack_list, direction)) {
cras_free_jack(jack, 0);
return -EIO;
}
if (direction == CRAS_STREAM_OUTPUT &&
(strstr(jack->gpio.device_name, "Headphone") ||
strstr(jack->gpio.device_name, "Headset")))
jack->mixer_output = cras_alsa_mixer_get_output_matching_name(
jack_list->mixer, "Headphone");
else if (direction == CRAS_STREAM_OUTPUT &&
strstr(jack->gpio.device_name, "HDMI"))
jack->mixer_output = cras_alsa_mixer_get_output_matching_name(
jack_list->mixer, "HDMI");
if (jack->ucm_device && direction == CRAS_STREAM_INPUT) {
char *control_name;
control_name = ucm_get_cap_control(jack->jack_list->ucm,
jack->ucm_device);
if (control_name)
jack->mixer_input =
cras_alsa_mixer_get_input_matching_name(
jack_list->mixer, control_name);
}
return cras_complete_gpio_jack(data, jack, switch_event);
}
static int
open_and_monitor_gpio_with_section(struct gpio_switch_list_data *data,
const char *pathname, unsigned switch_event)
{
struct cras_alsa_jack *jack;
struct cras_alsa_jack_list *jack_list = data->jack_list;
struct ucm_section *section = data->section;
enum CRAS_STREAM_DIRECTION direction = jack_list->direction;
int r;
r = create_jack_for_gpio(jack_list, pathname, section->jack_name,
switch_event, &jack);
if (r != 0)
return r;
jack->ucm_device = strdup(section->name);
if (!jack->ucm_device) {
cras_free_jack(jack, 0);
return -ENOMEM;
}
if (direction == CRAS_STREAM_OUTPUT)
jack->mixer_output = cras_alsa_mixer_get_control_for_section(
jack_list->mixer, section);
else if (direction == CRAS_STREAM_INPUT)
jack->mixer_input = cras_alsa_mixer_get_control_for_section(
jack_list->mixer, section);
return cras_complete_gpio_jack(data, jack, switch_event);
}
/* Monitor GPIO switches for this jack_list.
* Args:
* data - Data for GPIO switch search.
* dev_path - Device full path.
* dev_name - Device name.
* Returns:
* 0 for success, or negative on error. Assumes success if no jack is
* found, or if the jack could not be accessed.
*/
static int gpio_switches_monitor_device(struct gpio_switch_list_data *data,
const char *dev_path,
const char *dev_name)
{
static const int out_switches[] = { SW_HEADPHONE_INSERT,
SW_LINEOUT_INSERT };
static const int in_switches[] = { SW_MICROPHONE_INSERT };
int sw;
const int *switches = out_switches;
int num_switches = ARRAY_SIZE(out_switches);
int success = 1;
int rc = 0;
if (data->section && data->section->jack_switch >= 0) {
switches = &data->section->jack_switch;
num_switches = 1;
} else if (data->jack_list->direction == CRAS_STREAM_INPUT) {
switches = in_switches;
num_switches = ARRAY_SIZE(in_switches);
}
/* Assume that -EIO is returned for jacks that we shouldn't
* be looking at, but stop trying if we run into another
* type of error.
*/
for (sw = 0; (rc == 0 || rc == -EIO) && sw < num_switches; sw++) {
if (data->section)
rc = open_and_monitor_gpio_with_section(data, dev_path,
switches[sw]);
else
rc = open_and_monitor_gpio(data, dev_path, dev_name,
switches[sw]);
if (rc != 0 && rc != -EIO)
success = 0;
}
if (success)
return 0;
return rc;
}
static int gpio_switch_list_with_section(const char *dev_path,
const char *dev_name, void *arg)
{
struct gpio_switch_list_data *data =
(struct gpio_switch_list_data *)arg;
if (strcmp(dev_name, data->section->jack_name)) {
/* No match: continue searching. */
return 0;
}
data->rc = gpio_switches_monitor_device(data, dev_path, dev_name);
/* Found the only possible match: stop searching. */
return 1;
}
/* Match the given jack name to the given regular expression.
* Args:
* jack_name - The jack's name.
* re - Regular expression string.
* Returns:
* Non-zero for success, or 0 for failure.
*/
static int jack_matches_regex(const char *jack_name, const char *re)
{
regmatch_t m[1];
regex_t regex;
int rc;
rc = regcomp(&regex, re, REG_EXTENDED);
if (rc != 0) {
syslog(LOG_ERR, "Failed to compile regular expression: %s", re);
return 0;
}
rc = regexec(&regex, jack_name, ARRAY_SIZE(m), m, 0) == 0;
regfree(&regex);
return rc;
}
static int gpio_switch_list_by_matching(const char *dev_path,
const char *dev_name, void *arg)
{
struct gpio_switch_list_data *data =
(struct gpio_switch_list_data *)arg;
if (data->jack_list->direction == CRAS_STREAM_INPUT) {
if (!jack_matches_regex(dev_name, "^.*Mic Jack$") &&
!jack_matches_regex(dev_name, "^.*Headset Jack$")) {
/* Continue searching. */
return 0;
}
} else if (data->jack_list->direction == CRAS_STREAM_OUTPUT) {
if (!jack_matches_regex(dev_name, "^.*Headphone Jack$") &&
!jack_matches_regex(dev_name, "^.*Headset Jack$") &&
!jack_matches_regex(dev_name, "^.*HDMI Jack$")) {
/* Continue searching. */
return 0;
}
}
data->rc = gpio_switches_monitor_device(data, dev_path, dev_name);
/* Stop searching for failure. */
return data->rc;
}
/* Find ELD control for HDMI/DP gpio jack. */
static snd_hctl_elem_t *find_eld_control_by_dev_index(snd_hctl_t *hctl,
unsigned int dev_idx)
{
static const char eld_control_name[] = "ELD";
snd_ctl_elem_id_t *elem_id;
snd_ctl_elem_id_alloca(&elem_id);
snd_ctl_elem_id_clear(elem_id);
snd_ctl_elem_id_set_interface(elem_id, SND_CTL_ELEM_IFACE_PCM);
snd_ctl_elem_id_set_device(elem_id, dev_idx);
snd_ctl_elem_id_set_name(elem_id, eld_control_name);
return snd_hctl_find_elem(hctl, elem_id);
}
/* For non-gpio jack, check if it's of type hdmi/dp by
* matching jack name. */
static int is_jack_hdmi_dp(const char *jack_name)
{
// TODO(hychao): Use the information provided in UCM instead of
// name matching.
static const char *hdmi_dp = "HDMI";
return !!strstr(jack_name, hdmi_dp);
}
/* Find GPIO jacks for this jack_list.
* Args:
* jack_list - Jack list to add to.
* section - UCM section.
* result_jack - Filled with a pointer to the resulting cras_alsa_jack.
* Returns:
* 0 for success, or negative on error. Assumes success if no jack is
* found, or if the jack could not be accessed.
*/
static int find_gpio_jacks(struct cras_alsa_jack_list *jack_list,
struct ucm_section *section,
struct cras_alsa_jack **result_jack)
{
/* GPIO switches are on Arm-based machines, and are
* only associated with on-board devices.
*/
struct gpio_switch_list_data data;
int rc;
rc = wait_for_dev_input_access();
if (rc != 0) {
syslog(LOG_WARNING, "Could not access /dev/input/event0: %s",
strerror(rc));
return 0;
}
data.jack_list = jack_list;
data.section = section;
data.result_jack = NULL;
data.rc = 0;
if (section)
gpio_switch_list_for_each(gpio_switch_list_with_section, &data);
else
gpio_switch_list_for_each(gpio_switch_list_by_matching, &data);
if (result_jack) {
*result_jack = data.result_jack;
/* Find ELD control only for HDMI/DP gpio jack. */
if (*result_jack &&
is_jack_hdmi_dp((*result_jack)->gpio.device_name))
(*result_jack)->eld_control =
find_eld_control_by_dev_index(
jack_list->hctl,
jack_list->device_index);
}
return data.rc;
}
/* Callback from alsa when a jack control changes. This is registered with
* snd_hctl_elem_set_callback in find_jack_controls and run by calling
* snd_hctl_handle_events in alsa_control_event_pending below.
* Args:
* elem - The ALSA control element that has changed.
* mask - unused.
*/
static int hctl_jack_cb(snd_hctl_elem_t *elem, unsigned int mask)
{
const char *name;
snd_ctl_elem_value_t *elem_value;
struct cras_alsa_jack *jack;
jack = snd_hctl_elem_get_callback_private(elem);
if (jack == NULL) {
syslog(LOG_ERR, "Invalid jack from control event.");
return -EINVAL;
}
snd_ctl_elem_value_alloca(&elem_value);
snd_hctl_elem_read(elem, elem_value);
name = snd_hctl_elem_get_name(elem);
syslog(LOG_DEBUG, "Jack %s %s", name,
snd_ctl_elem_value_get_boolean(elem_value, 0) ? "plugged" :
"unplugged");
jack_state_change_cb(jack, 1);
return 0;
}
/* Determines the device associated with this jack if any. If the device cannot
* be determined (common case), assume device 0. */
static unsigned int hctl_jack_device_index(const char *name)
{
/* Look for the substring 'pcm=<device number>' in the element name. */
static const char pcm_search[] = "pcm=";
const char *substr;
int device_index;
substr = strstr(name, pcm_search);
if (substr == NULL)
return 0;
substr += ARRAY_SIZE(pcm_search) - 1;
if (*substr == '\0')
return 0;
device_index = atoi(substr);
if (device_index < 0)
return 0;
return (unsigned int)device_index;
}
/* Checks if the given control name is in the supplied list of possible jack
* control base names. */
static int is_jack_control_in_list(const char *const *list,
unsigned int list_length,
const char *control_name)
{
unsigned int i;
for (i = 0; i < list_length; i++)
if (strncmp(control_name, list[i], strlen(list[i])) == 0)
return 1;
return 0;
}
/* Check if the given name is a jack created for the connector control of a
* input/output terminal entity on a USB Audio Class 2.0 device. */
static int is_jack_uac2(const char *jack_name,
enum CRAS_STREAM_DIRECTION direction)
{
return jack_matches_regex(jack_name, direction == CRAS_STREAM_OUTPUT ?
"^.* - Output Jack$" :
"^.* - Input Jack$");
}
/* Looks for any JACK controls. Monitors any found controls for changes and
* decides to route based on plug/unlpug events. */
static int find_jack_controls(struct cras_alsa_jack_list *jack_list)
{
snd_hctl_elem_t *elem;
struct cras_alsa_jack *jack;
const char *name;
static const char *const output_jack_base_names[] = {
"Headphone Jack",
"Front Headphone Jack",
"HDMI/DP",
"Speaker Phantom Jack",
};
static const char *const input_jack_base_names[] = {
"Mic Jack",
};
const char *const *jack_names;
unsigned int num_jack_names;
if (!jack_list->hctl) {
syslog(LOG_WARNING, "Can't search hctl for jacks.");
return 0;
}
if (jack_list->direction == CRAS_STREAM_OUTPUT) {
jack_names = output_jack_base_names;
num_jack_names = ARRAY_SIZE(output_jack_base_names);
} else {
jack_names = input_jack_base_names;
num_jack_names = ARRAY_SIZE(input_jack_base_names);
}
for (elem = snd_hctl_first_elem(jack_list->hctl); elem != NULL;
elem = snd_hctl_elem_next(elem)) {
snd_ctl_elem_iface_t iface;
iface = snd_hctl_elem_get_interface(elem);
if (iface != SND_CTL_ELEM_IFACE_CARD)
continue;
name = snd_hctl_elem_get_name(elem);
if (!is_jack_control_in_list(jack_names, num_jack_names,
name) &&
!is_jack_uac2(name, jack_list->direction))
continue;
if (hctl_jack_device_index(name) != jack_list->device_index)
continue;
jack = cras_alloc_jack(0);
if (jack == NULL)
return -ENOMEM;
jack->elem = elem;
jack->jack_list = jack_list;
DL_APPEND(jack_list->jacks, jack);
snd_hctl_elem_set_callback(elem, hctl_jack_cb);
snd_hctl_elem_set_callback_private(elem, jack);
if (jack_list->direction == CRAS_STREAM_OUTPUT)
jack->mixer_output =
cras_alsa_mixer_get_output_matching_name(
jack_list->mixer, name);
if (jack_list->ucm)
jack->ucm_device = ucm_get_dev_for_jack(
jack_list->ucm, name, jack_list->direction);
if (jack->ucm_device &&
jack_list->direction == CRAS_STREAM_INPUT) {
char *control_name;
control_name = ucm_get_cap_control(jack->jack_list->ucm,
jack->ucm_device);
if (control_name)
jack->mixer_input =
cras_alsa_mixer_get_input_matching_name(
jack_list->mixer, control_name);
}
if (jack->ucm_device) {
jack->override_type_name = ucm_get_override_type_name(
jack->jack_list->ucm, jack->ucm_device);
}
}
/* Look up ELD controls */
DL_FOREACH (jack_list->jacks, jack) {
if (jack->is_gpio || jack->eld_control)
continue;
name = snd_hctl_elem_get_name(jack->elem);
if (!is_jack_hdmi_dp(name))
continue;
jack->eld_control = find_eld_control_by_dev_index(
jack_list->hctl, jack_list->device_index);
}
return 0;
}
/*
* Exported Interface.
*/
int cras_alsa_jack_list_find_jacks_by_name_matching(
struct cras_alsa_jack_list *jack_list)
{
int rc;
rc = find_jack_controls(jack_list);
if (rc != 0)
return rc;
return find_gpio_jacks(jack_list, NULL, NULL);
}
static int find_hctl_jack_for_section(struct cras_alsa_jack_list *jack_list,
struct ucm_section *section,
struct cras_alsa_jack **result_jack)
{
snd_hctl_elem_t *elem;
snd_ctl_elem_id_t *elem_id;
struct cras_alsa_jack *jack;
if (!jack_list->hctl) {
syslog(LOG_WARNING, "Can't search hctl for jacks.");
return -ENODEV;
}
snd_ctl_elem_id_alloca(&elem_id);
snd_ctl_elem_id_clear(elem_id);
snd_ctl_elem_id_set_interface(elem_id, SND_CTL_ELEM_IFACE_CARD);
snd_ctl_elem_id_set_device(elem_id, jack_list->device_index);
snd_ctl_elem_id_set_name(elem_id, section->jack_name);
elem = snd_hctl_find_elem(jack_list->hctl, elem_id);
if (!elem)
return -ENOENT;
syslog(LOG_DEBUG, "Found Jack: %s for %s", section->jack_name,
section->name);
jack = cras_alloc_jack(0);
if (jack == NULL)
return -ENOMEM;
jack->elem = elem;
jack->jack_list = jack_list;
jack->ucm_device = strdup(section->name);
if (!jack->ucm_device) {
free(jack);
return -ENOMEM;
}
if (jack_list->direction == CRAS_STREAM_OUTPUT)
jack->mixer_output = cras_alsa_mixer_get_control_for_section(
jack_list->mixer, section);
else if (jack_list->direction == CRAS_STREAM_INPUT)
jack->mixer_input = cras_alsa_mixer_get_control_for_section(
jack_list->mixer, section);
snd_hctl_elem_set_callback(elem, hctl_jack_cb);
snd_hctl_elem_set_callback_private(elem, jack);
DL_APPEND(jack_list->jacks, jack);
if (result_jack)
*result_jack = jack;
if (!strcmp(jack->ucm_device, "HDMI") ||
!strcmp(jack->ucm_device, "DP"))
return 0;
/* Look up ELD control. */
jack->eld_control = find_eld_control_by_dev_index(
jack_list->hctl, jack_list->device_index);
return 0;
}
/*
* Exported Interface.
*/
int cras_alsa_jack_list_add_jack_for_section(
struct cras_alsa_jack_list *jack_list, struct ucm_section *ucm_section,
struct cras_alsa_jack **result_jack)
{
if (result_jack)
*result_jack = NULL;
if (!ucm_section)
return -EINVAL;
if (!ucm_section->jack_name) {
/* No jacks defined for this device. */
return 0;
}
if (!ucm_section->jack_type) {
syslog(LOG_ERR,
"Must specify the JackType for jack '%s' in '%s'.",
ucm_section->jack_name, ucm_section->name);
return -EINVAL;
}
if (!strcmp(ucm_section->jack_type, "hctl")) {
return find_hctl_jack_for_section(jack_list, ucm_section,
result_jack);
} else if (!strcmp(ucm_section->jack_type, "gpio")) {
return find_gpio_jacks(jack_list, ucm_section, result_jack);
} else {
syslog(LOG_ERR, "Invalid JackType '%s' in '%s'.",
ucm_section->jack_type, ucm_section->name);
return -EINVAL;
}
}
struct cras_alsa_jack_list *
cras_alsa_jack_list_create(unsigned int card_index, const char *card_name,
unsigned int device_index, int is_first_device,
struct cras_alsa_mixer *mixer,
struct cras_use_case_mgr *ucm, snd_hctl_t *hctl,
enum CRAS_STREAM_DIRECTION direction,
jack_state_change_callback *cb, void *cb_data)
{
struct cras_alsa_jack_list *jack_list;
if (direction != CRAS_STREAM_INPUT && direction != CRAS_STREAM_OUTPUT)
return NULL;
jack_list = (struct cras_alsa_jack_list *)calloc(1, sizeof(*jack_list));
if (jack_list == NULL)
return NULL;
jack_list->change_callback = cb;
jack_list->callback_data = cb_data;
jack_list->mixer = mixer;
jack_list->ucm = ucm;
jack_list->hctl = hctl;
jack_list->card_index = card_index;
jack_list->card_name = card_name;
jack_list->device_index = device_index;
jack_list->is_first_device = is_first_device;
jack_list->direction = direction;
return jack_list;
}
void cras_alsa_jack_list_destroy(struct cras_alsa_jack_list *jack_list)
{
struct cras_alsa_jack *jack;
if (jack_list == NULL)
return;
DL_FOREACH (jack_list->jacks, jack) {
DL_DELETE(jack_list->jacks, jack);
cras_free_jack(jack, 1);
}
free(jack_list);
}
int cras_alsa_jack_list_has_hctl_jacks(struct cras_alsa_jack_list *jack_list)
{
struct cras_alsa_jack *jack;
if (!jack_list)
return 0;
DL_FOREACH (jack_list->jacks, jack) {
if (!jack->is_gpio)
return 1;
}
return 0;
}
struct mixer_control *
cras_alsa_jack_get_mixer_output(const struct cras_alsa_jack *jack)
{
if (jack == NULL)
return NULL;
return jack->mixer_output;
}
struct mixer_control *
cras_alsa_jack_get_mixer_input(const struct cras_alsa_jack *jack)
{
return jack ? jack->mixer_input : NULL;
}
void cras_alsa_jack_list_report(const struct cras_alsa_jack_list *jack_list)
{
struct cras_alsa_jack *jack;
if (jack_list == NULL)
return;
DL_FOREACH (jack_list->jacks, jack)
if (jack->is_gpio)
gpio_switch_initial_state(jack);
else
hctl_jack_cb(jack->elem, 0);
}
const char *cras_alsa_jack_get_name(const struct cras_alsa_jack *jack)
{
if (jack == NULL)
return NULL;
if (jack->is_gpio)
return jack->gpio.device_name;
return snd_hctl_elem_get_name(jack->elem);
}
const char *cras_alsa_jack_get_ucm_device(const struct cras_alsa_jack *jack)
{
return jack->ucm_device;
}
void cras_alsa_jack_update_monitor_name(const struct cras_alsa_jack *jack,
char *name_buf, unsigned int buf_size)
{
snd_ctl_elem_value_t *elem_value;
snd_ctl_elem_info_t *elem_info;
const char *buf = NULL;
int count;
int mnl = 0;
if (!jack->eld_control) {
if (jack->edid_file)
get_jack_edid_monitor_name(jack, name_buf, buf_size);
return;
}
snd_ctl_elem_info_alloca(&elem_info);
if (snd_hctl_elem_info(jack->eld_control, elem_info) < 0)
goto fallback_jack_name;
count = snd_ctl_elem_info_get_count(elem_info);
if (count <= ELD_MNL_OFFSET)
goto fallback_jack_name;
snd_ctl_elem_value_alloca(&elem_value);
if (snd_hctl_elem_read(jack->eld_control, elem_value) < 0)
goto fallback_jack_name;
buf = (const char *)snd_ctl_elem_value_get_bytes(elem_value);
mnl = buf[ELD_MNL_OFFSET] & ELD_MNL_MASK;
if (count < ELD_MONITOR_NAME_OFFSET + mnl)
goto fallback_jack_name;
/* Note that monitor name string does not contain terminate character.
* Check monitor name length with name buffer size.
*/
if (mnl >= buf_size)
mnl = buf_size - 1;
strncpy(name_buf, buf + ELD_MONITOR_NAME_OFFSET, mnl);
name_buf[mnl] = '\0';
return;
fallback_jack_name:
buf = cras_alsa_jack_get_name(jack);
strncpy(name_buf, buf, buf_size - 1);
return;
}
void cras_alsa_jack_update_node_type(const struct cras_alsa_jack *jack,
enum CRAS_NODE_TYPE *type)
{
if (!jack->override_type_name)
return;
if (!strcmp(jack->override_type_name, "Internal Speaker"))
*type = CRAS_NODE_TYPE_INTERNAL_SPEAKER;
return;
}
void cras_alsa_jack_enable_ucm(const struct cras_alsa_jack *jack, int enable)
{
if (jack && jack->ucm_device)
ucm_set_enabled(jack->jack_list->ucm, jack->ucm_device, enable);
}