/* 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 #include #include #include #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. */ 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 *dsp_name; 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((void *)jack->dsp_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) { 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, const char *card_name, enum CRAS_STREAM_DIRECTION direction) { const char* target_device_name = NULL; char current_device_name[CRAS_IODEV_NAME_BUFFER_SIZE]; unsigned int rc; /* 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; /* Look for device name specified in a device section of UCM. */ target_device_name = ucm_get_device_name_for_dev( jack_list->ucm, jack->ucm_device, direction); if (!target_device_name) return jack_list->is_first_device; syslog(LOG_DEBUG, "Matching GPIO jack, target device name: %s, " "current card name: %s, device index: %zu\n", target_device_name, card_name, jack_list->device_index); /* Device name of format "hw:,", should fit * in the string of size CRAS_IODEV_NAME_BUFFER_SIZE.*/ snprintf(current_device_name, sizeof(current_device_name), "hw:%s,%zu", card_name, jack_list->device_index); rc = !strcmp(current_device_name, target_device_name); free((void*)target_device_name); return rc; } 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; enum CRAS_STREAM_DIRECTION direction = jack_list->direction; int r; if (jack->ucm_device) { jack->edid_file = ucm_get_edid_file_for_dev(jack_list->ucm, jack->ucm_device); jack->dsp_name = ucm_get_dsp_name( jack->jack_list->ucm, jack->ucm_device, direction); } 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); 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; const char *card_name = jack_list->card_name; 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, card_name, 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(®ex, re, REG_EXTENDED); if (rc != 0) { syslog(LOG_ERR, "Failed to compile regular expression: %s", re); return 0; } rc = regexec(®ex, jack_name, ARRAY_SIZE(m), m, 0) == 0; regfree(®ex); 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 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; 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=' 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; } /* 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) { static const char *hdmi_dp = "HDMI/DP"; return strncmp(jack_name, hdmi_dp, strlen(hdmi_dp)) == 0; } /* 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; } /* 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", }; static const char eld_control_name[] = "ELD"; const char * const *jack_names; unsigned int num_jack_names; char device_name[6]; 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)) 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); syslog(LOG_DEBUG, "Found Jack: %s for %s", name, device_name); 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->dsp_name = ucm_get_dsp_name( jack->jack_list->ucm, jack->ucm_device, jack_list->direction); 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; for (elem = snd_hctl_first_elem(jack_list->hctl); elem != NULL; elem = snd_hctl_elem_next(elem)) { if (strcmp(snd_hctl_elem_get_name(elem), eld_control_name)) continue; if (snd_hctl_elem_get_device(elem) != jack_list->device_index) continue; jack->eld_control = elem; break; } } 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) { static const char eld_control_name[] = "ELD"; 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); jack->dsp_name = ucm_get_dsp_name( jack->jack_list->ucm, jack->ucm_device, jack_list->direction); 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. */ snd_ctl_elem_id_set_name(elem_id, eld_control_name); elem = snd_hctl_find_elem(jack_list->hctl, elem_id); if (elem) jack->eld_control = elem; 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; } const char *cras_alsa_jack_get_dsp_name(const struct cras_alsa_jack *jack) { if (jack == NULL) return NULL; return jack->dsp_name; } 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); }