chrontel: control audio to allow built-in speakers when external display does
not support audio.

Chrontel daemon can determine whether external display provides audio
capabilities via its EDID.  Previously the audio codec driver managed the audio
sink without this knowledge making it impossible to use the built-in speakers if
the display didn't support audio.

This change and related CL's allows the chrontel daemon to manage audio sink
when external display is attached such that built-in speakers can be utilized.
Headphones/line out still have highest priority.

Related CL's:
-------------
http://gerrit.chromium.org/gerrit/4786
http://gerrit.chromium.org/gerrit/4789

BUG=chrome-os-partner:3719
TEST=manual,

- installing
  USE="use_alsa_control" emerge-<dut>
  gmerge -n chrontel

- testing (make sure audio sent correctly during)
  - start/stop daemon
  - insert/remove headphones
  - change /etc/init/chrontel.conf's ch7036_monitor invocation to include switch
    -M9 which forces DVI ( no external audio support )

Change-Id: I563785b83dc3b519cd8c5ffd653c80cd116bd43a
Reviewed-on: http://gerrit.chromium.org/gerrit/4788
Reviewed-by: Mark Hayter <mdhayter@chromium.org>
Tested-by: Mark Hayter <mdhayter@chromium.org>
diff --git a/Makefile b/Makefile
index 9840d23..3a5936d 100644
--- a/Makefile
+++ b/Makefile
@@ -8,11 +8,11 @@
 PKG_CONFIG ?= pkg-config
 
 INCLUDE_DIRS = $(shell $(PKG_CONFIG) --cflags x11 xrandr)
-LIB_DIRS = $(shell $(PKG_CONFIG) --libs x11 xrandr xext)
+LIB_DIRS = $(shell $(PKG_CONFIG) --libs x11 xrandr xext alsa)
 
 MON=ch7036_monitor
 BUG=ch7036_debug
-OBJECTS=ch7036_access.o GenTableCH7036.o edid_utils.o xrr_utils.o
+OBJECTS=ch7036_access.o GenTableCH7036.o edid_utils.o xrr_utils.o audio_utils.o
 
 all: $(MON) $(BUG)
 
@@ -27,9 +27,5 @@
 	$(CC) $(CCFLAGS) $(INCLUDE_DIRS) $(LIB_DIRS) $^ $(LIBS) $(LDFLAGS) \
                 -o $@
 
-
 clean:
 	@rm -f $(MON) $(BUG) $(OBJECTS)
-
-
-
diff --git a/audio_utils.c b/audio_utils.c
new file mode 100644
index 0000000..9c16d5a
--- /dev/null
+++ b/audio_utils.c
@@ -0,0 +1,256 @@
+// Copyright (c) 2011 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 <assert.h>
+#include <alsa/asoundlib.h>
+#include <dirent.h>
+#include <linux/input.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "audio_utils.h"
+
+#ifdef USE_ALSA_CONTROL
+static int aud_verbose = 0;
+static struct aud_jack_event hp_jack;
+static char card[64] = "default";
+
+static int aud_switch(enum aud_cmd cmd, const char *name, int *value)
+{
+  int err = 0;
+  snd_mixer_t *handle;
+  snd_mixer_selem_id_t *sid;
+  snd_mixer_elem_t *elem;
+  snd_mixer_selem_id_alloca(&sid);
+
+  if ((err = snd_mixer_open(&handle, 0)) < 0) {
+    aud_errno("mixer %s open", card);
+    return err;
+  }
+  if ((err = snd_mixer_attach(handle, card)) < 0) {
+    aud_errno("mixer attach %s", card);
+    snd_mixer_close(handle);
+    return err;
+  }
+  if ((err = snd_mixer_selem_register(handle, NULL, NULL)) < 0) {
+    aud_errno("mixer register");
+    snd_mixer_close(handle);
+    return err;
+  }
+  if ((err = snd_mixer_load(handle)) < 0) {
+    aud_errno("mixer %s load", card);
+    snd_mixer_close(handle);
+    return err;
+  }
+  for (elem = snd_mixer_first_elem(handle); elem;
+       elem = snd_mixer_elem_next(elem)) {
+    snd_mixer_selem_get_id(elem, sid);
+    if (!(strcmp(snd_mixer_selem_id_get_name(sid), name))) {
+      if (aud_verbose)
+        printf("Audio: found %s audio switch\n", name);
+      if (snd_mixer_selem_has_playback_switch(elem)) {
+        switch (cmd) {
+          case get:
+            err = snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_MONO,
+                                                      value);
+            if (err < 0)
+              aud_errno("getting %s", name);
+            break;
+          case set:
+            err = snd_mixer_selem_set_playback_switch_all(elem, *value);
+            if (err < 0)
+              aud_errno("setting %s -> %d", name, *value);
+            break;
+          default:
+            aud_error("unknown switch cmd %d for %s\n", cmd, name);
+            err = -1;
+            break;
+        }
+      }
+    }
+  }
+  snd_mixer_close(handle);
+  return err;
+}
+
+static int get_master_switch()
+{
+  int value = 0;
+  aud_switch(get, "Master", &value);
+  return value;
+}
+
+static int set_master_switch(int value)
+{
+  int *pval = &value;
+  return aud_switch(set, "Master", pval);
+}
+
+static int open_jack_event(int etype)
+{
+  DIR *ev_dir;
+  struct dirent *ev_entry;
+  int fd = -1;
+
+  assert(etype & SW_HEADPHONE_INSERT);
+
+  if((ev_dir = opendir(INPUT_DIR)) == NULL) {
+    aud_errno("opening dir %s", INPUT_DIR);
+    return -1;
+  }
+  if (chdir(INPUT_DIR)) {
+    aud_errno("chdir to %s", INPUT_DIR);
+    return -1;
+  }
+
+  while ((ev_entry = readdir(ev_dir))) {
+    if ((ev_entry->d_type == DT_CHR) &&
+        !(strncmp(ev_entry->d_name, "event", 5))) {
+
+      if (access(ev_entry->d_name, R_OK) < 0) {
+        aud_errno("read access %s", ev_entry->d_name);
+        return -1;
+      }
+
+      if ((fd = open(ev_entry->d_name, O_RDONLY)) < 0) {
+        aud_errno("opening event %s", ev_entry->d_name);
+        return -1;
+      }
+
+      unsigned long events[NBITS(EV_MAX)];
+      memset(events, 0, sizeof(events));
+      if (ioctl(fd, EVIOCGBIT(0, EV_MAX), events) < 0) {
+        aud_errno("ioctl EVIOCGBIT for events");
+        return -1;
+      }
+
+      if (IS_BIT_SET(EV_SW, events)) {
+        unsigned long sw_events[NBITS(SW_MAX)];
+        memset(sw_events, 0, sizeof(sw_events));
+
+        if (ioctl(fd, EVIOCGBIT(EV_SW, SW_MAX), sw_events) < 0) {
+          aud_errno("ioctl EVIOCGBIT for SW events");
+          return -1;
+        }
+        if (IS_BIT_SET(etype, sw_events)) {
+          break;
+        }
+      }
+    }
+  }
+  return fd;
+}
+
+static int open_jack_hp_event(struct aud_jack_event *jack) {
+  jack->etype = SW_HEADPHONE_INSERT;
+  jack->fd = open_jack_event(SW_HEADPHONE_INSERT);
+  jack->status = unknown;
+  return jack->fd;
+}
+
+static void get_jack_status(struct aud_jack_event *jack) {
+  unsigned long events[NBITS(SW_MAX)];
+  assert(jack->fd > 0);
+  memset(events, 0, sizeof(events));
+  ioctl(jack->fd, EVIOCGSW(sizeof(events)), events);
+  jack->status = IS_BIT_SET(hp_jack.etype, events);
+}
+
+static void close_jack_event(struct aud_jack_event *jack)
+{
+  close(jack->fd);
+}
+
+#define FORCE_MUTE_FILE "/var/run/chrontel/forced_mute"
+
+static int get_force_mute()
+{
+  return (access(FORCE_MUTE_FILE, R_OK) == 0);
+}
+
+static void set_force_mute()
+{
+  int fd;
+  do {
+    fd = creat(FORCE_MUTE_FILE, S_IRUSR | S_IWUSR);
+  } while ((fd == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK)));
+  if (fd == -1) {
+    aud_errno("creating %s", FORCE_MUTE_FILE);
+  } else {
+    close(fd);
+  }
+  set_master_switch(0);
+  if (aud_verbose) printf("Audio: muted Master\n");
+}
+
+static void clear_force_mute()
+{
+  int err;
+  do {
+    err = remove(FORCE_MUTE_FILE);
+  } while ((err == -1 ) && ((errno == EAGAIN) || (errno == EWOULDBLOCK)));
+  if (err == -1)
+    aud_errno("removing %s", FORCE_MUTE_FILE);
+  set_master_switch(1);
+  if (aud_verbose) printf("Audio: unmuted Master\n");
+}
+
+#endif /* USE_ALSA_CONTROL */
+
+int audio_init(int verbose)
+{
+  int err = 0;
+#ifdef USE_ALSA_CONTROL
+
+  aud_verbose = verbose;
+  if ((err = open_jack_hp_event(&hp_jack)) < 0) {
+    aud_error("opening hp jack event\n");
+  } else {
+    get_jack_status(&hp_jack);
+  }
+
+#endif
+  return err;
+}
+
+void audio_to_hdmi(int enable)
+{
+#ifdef USE_ALSA_CONTROL
+
+  if (aud_verbose) printf("Audio: Set HDMI audio output to %s\n",
+                          enable ? "Enable" : "Disable");
+
+  if (enable) {
+    get_jack_status(&hp_jack);
+    if (aud_verbose) printf("Audio: headphone is %s\n",
+                            hp_jack.status ? "plugged-in" : "unplugged");
+    if (hp_jack.status == unplugged)
+      set_force_mute();
+    else if (get_force_mute())
+      clear_force_mute();
+  } else {
+    if (get_force_mute())
+      clear_force_mute();
+  }
+
+#endif
+}
+
+void audio_check_jack(int enable)
+{
+#ifdef USE_ALSA_CONTROL
+  if (enable == 0)
+    return;
+
+  enum jack_status cur_status = hp_jack.status;
+  get_jack_status(&hp_jack);
+  if (hp_jack.status != cur_status)
+    audio_to_hdmi(enable);
+
+#endif
+}
diff --git a/audio_utils.h b/audio_utils.h
new file mode 100644
index 0000000..9435445
--- /dev/null
+++ b/audio_utils.h
@@ -0,0 +1,49 @@
+// Copyright (c) 2011 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.
+
+#ifndef __AUDIO_UTILS_H__
+#define __AUDIO_UTILS_H__
+
+#include <strings.h>
+#include <errno.h>
+
+#define INPUT_DIR "/dev/input"
+#define BITS_PER_LONG (sizeof(long) * 8)
+#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)] >> OFF(bit)) & 1)
+
+#define aud_error(...) do {                                             \
+  fprintf(stderr, "Audio: Error: %s:%u :: ", __FILE__, __LINE__);       \
+  fprintf(stderr, __VA_ARGS__); } while (0)
+
+#define aud_errno(...) do {                                             \
+  fprintf(stderr, "Audio: Error: %s:%u :: ", __FILE__, __LINE__);       \
+  fprintf(stderr, __VA_ARGS__);                                         \
+  fprintf(stderr, ", err:%s\n", strerror(errno));} while (0)
+
+enum aud_cmd {
+  get=0,
+  set=1,
+};
+
+enum jack_status {
+  unplugged=0,
+  plugged=1,
+  unknown=2,
+};
+
+struct aud_jack_event {
+  int fd;
+  int etype;
+  enum jack_status status;
+};
+
+int audio_init(int verbose);
+void audio_to_hdmi(int enable);
+void audio_check_jack(int enable);
+
+#endif
diff --git a/ch7036_monitor.c b/ch7036_monitor.c
index d5ffcd0..12f373f 100644
--- a/ch7036_monitor.c
+++ b/ch7036_monitor.c
@@ -25,7 +25,7 @@
 #error Only tested with xrandr 1.2
 #endif
 
-
+#include "audio_utils.h"
 #include "ch7036.h"
 #include "edid_utils.h"
 #include "xrr_utils.h"
@@ -993,6 +993,8 @@
   lcd_timing.vw = xmode->vSyncEnd - xmode->vSyncStart;
   lcd_timing.scale = MK_SCALE(0, 0);
 
+  audio_init(verbose);
+
   while (1) {
     int new_width;
     int new_height;
@@ -1197,6 +1199,7 @@
       if (verbose) printf("Change X active to %dx%d\n", new_width, new_height);
       set_x_size(dpy, root, new_width, new_height, use_black_window, bwin);
     }
+    audio_to_hdmi(enable_audio);
     if (verbose) printf("Monitor should be on!\n");
     if ((edidlog = fopen(EDID_LOG, "w")) != NULL) {
       fprintf(edidlog, "%sI output %dx%d%s. Use %dx%d on local LCD.\n",
@@ -1238,6 +1241,7 @@
         ch_write_reg_sequence(i2cdev, reglist);
         ch_calculate_incs(i2cdev);
         ch_monitor_on(i2cdev, output_hdmi, verbose);
+        audio_to_hdmi(enable_audio);
         hdmiOn = 1;
         /* Machine was suspended, so the firmware was lost */
         needReload = reloadfw(i2cdev, fwfile, argv[0], gpiodev, 0, verbose);
@@ -1273,6 +1277,7 @@
           hdmiOn = 1;
         }
       }
+      audio_check_jack(enable_audio);
       sleep(gpiodev ? YIELD_GPIO_DETECT_SECS : YIELD_CHRONTEL_DETECT_SECS);
       if (test_flags & CHTEST_FORCEDETECT)
         mondetect = 1;
@@ -1294,6 +1299,7 @@
     }
     if (hdmiOn && !needReload) {
       ch_monitor_off_keep_ddc(i2cdev);
+      audio_to_hdmi(0);
       hdmiOn = 0;
       if (verbose) printf("Monitor should be off!\n");
       sleep(YIELD_CHRONTEL_TURN_OFF_SECS);
diff --git a/chrontel.conf b/chrontel.conf
index 7bbf49b..7e95f45 100644
--- a/chrontel.conf
+++ b/chrontel.conf
@@ -11,6 +11,8 @@
 # sadly, these can't reference each other.
 env DATA_DIR=/home/chronos
 env LOGBASE=/var/log/ch7036_monitor
+env RUNDIR=/var/run/chrontel
+env RUNFILES="forced_mute"
 env XAUTHORITY=/home/chronos/.Xauthority
 env DISPLAY=:0.0
 # keep these consistent, make second empty for pure ch7036 detection
@@ -47,6 +49,16 @@
 # Note you get an error if you try to re-export a gpio
 pre-start script
   echo "No HDMI support" > /var/log/hdmi_edid.log 
+  if [ ! -d $RUNDIR ] ; then
+     mkdir $RUNDIR
+  else
+     for name in $RUNFILES ; do
+       if [ -e $RUNDIR/$name ] ; then
+         rm $RUNDIR/$name
+       fi
+     done
+  fi
+  test -d $RUNDIR
   test -e /usr/bin/ch7036_monitor
   /sbin/modprobe i2c-dev
   /sbin/modprobe i2c-i801
@@ -71,3 +83,13 @@
   echo Made symlink ${dv%/name} $I2C >> /tmp/chprobe
   /usr/bin/ch7036_monitor -p -d $I2C -v >> /tmp/chprobe 2>&1
 end script
+
+post-stop script
+  if [ -d $RUNDIR ] ; then
+     for name in $RUNFILES ; do
+       if [ -e $RUNDIR/$name ] ; then
+         rm $RUNDIR/$name
+       fi
+     done
+  fi
+end script