Add an audio/video synchronization utility

On Chameleon, audio capturing and video capturing work separately.
To know the time difference between audio/video capturing, we can
monitor the changes of the audio page count and the video frame count.

BUG=None
TEST=manual test.
  Use test_server to test StartMonitoringAudioVideoCapturingDelay() and
  GetAudioVideoCapturingDelay(), note that the edid should be equipped
  with audio.
  >>> for i in xrange(10):
  ...   p.StartMonitoringAudioVideoCapturingDelay()
  ...   p.StartCapturingVideo(3)
  ...   p.StartCapturingAudio(3)
  ...   time.sleep(2)
  ...   p.StopCapturingVideo()
  ...   p.StopCapturingAudio(3)
  ...   print p.GetAudioVideoCapturingDelay()
  ...

Change-Id: I9b213964afed51a2e9c7b9c56ddc8b6002ef73bd
Reviewed-on: https://chromium-review.googlesource.com/371158
Commit-Ready: Chen-hao Chang <haocc@google.com>
Tested-by: Chen-hao Chang <haocc@google.com>
Reviewed-by: Wai-Hong Tam <waihong@google.com>
diff --git a/Makefile b/Makefile
index 37ffde8..0f3a794 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,8 @@
 directories:
 	@mkdir -p $(BINDIR)
 
-BINARIES = $(BINDIR)/histogram $(BINDIR)/hpd_control $(BINDIR)/pixeldump
+BINARIES = $(BINDIR)/histogram $(BINDIR)/hpd_control $(BINDIR)/pixeldump \
+	  $(BINDIR)/avsync
 
 .PHONY: binaries
 binaries: $(BINARIES)
diff --git a/chameleond/drivers/fpga_tio.py b/chameleond/drivers/fpga_tio.py
index 40eb2e8..99f13c4 100644
--- a/chameleond/drivers/fpga_tio.py
+++ b/chameleond/drivers/fpga_tio.py
@@ -107,6 +107,7 @@
     self._selected_input = None
     self._selected_output = None
     self._captured_params = {}
+    self._process = None
     # Reserve index 0 as the default EDID.
     self._all_edids = [self._ReadDefaultEdid()]
 
@@ -556,6 +557,47 @@
       self._selected_output = port_id
     self._flows[port_id].Do_FSM()
 
+  def StartMonitoringAudioVideoCapturingDelay(self):
+    """Starts an audio/video synchronization utility
+
+    The example of usage:
+      chameleon.StartMonitoringAudioVideoCapturingDelay()
+      chameleon.StartCapturingVideo(hdmi_input)
+      chameleon.StartCapturingAudio(hdmi_input)
+      time.sleep(2)
+      chameleon.StopCapturingVideo()
+      chameleon.StopCapturingAudio(hdmi_input)
+      delay = chameleon.GetAudioVideoCapturingDelay()
+    """
+    self._process = system_tools.SystemTools.RunInSubprocess('avsync')
+
+  def GetAudioVideoCapturingDelay(self):
+    """Get the time interval between the first audio/video cpatured data
+
+    Returns:
+      A floating points indicating the time interval between the first
+      audio/video data captured. If the result is negative, then the first
+      video data is earlier, otherwise the first audio data is earlier.
+
+    Raises:
+      DriverError if there is no output from the monitoring process.
+    """
+
+    if self._process.poll() == None:
+      self._process.terminate()
+      raise DriverError('The monitoring process has not finished.')
+
+    return_code, out, err = system_tools.SystemTools.GetSubprocessOutput(
+        self._process)
+
+    if return_code != 0 or err:
+      raise DriverError('Runtime error in the monitoring process')
+
+    if not out:
+      raise DriverError('No output from the monitoring process.')
+
+    return float(out)
+
   @_VideoMethod
   def DumpPixels(self, port_id, x=None, y=None, width=None, height=None):
     """Dumps the raw pixel array of the selected area.
@@ -839,7 +881,7 @@
                   value of GetCapturedFrameCount.
 
     Returns:
-      The list of checksums of frames.
+      The list of histograms of frames.
     """
     return self._GetCapturedSignals('GetHistograms', start_index, stop_index)
 
diff --git a/chameleond/utils/memory_dumper.py b/chameleond/utils/memory_dumper.py
index c8341a6..9394c4d 100644
--- a/chameleond/utils/memory_dumper.py
+++ b/chameleond/utils/memory_dumper.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
 # Copyright 2016 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.
@@ -6,11 +5,7 @@
 
 import logging
 import multiprocessing
-import subprocess
-import sys
-import time
 
-from chameleond.utils import mem
 from chameleond.utils import system_tools
 
 
@@ -35,6 +30,7 @@
     self._last_page_count = None
     self._last_last_page_count = None
     self._current_page_count = None
+    self._page_count_in_this_period = None
     self._file_path = file_path
     self._adump = adump
     self._stop_event = multiprocessing.Event()
@@ -45,8 +41,13 @@
     self._stop_event.set()
 
   def run(self):
-    """Runs the periodic dumping process."""
-    self._last_page_count = self._adump.GetCurrentPageCount()
+    """Runs the periodic dumping process.
+
+    Note that the audio page count should really start from 0, and this
+    should not be called too lately, or the page count in the first period
+    may be greater than MAX_DUMP_PAGES.
+    """
+    self._last_page_count = 0
     while True:
       self._stop_event.wait(self._DUMP_PERIOD_SECS)
       if self._stop_event.is_set():
@@ -177,7 +178,7 @@
     start_address = (self._adump.start_address +
                      start_page_index * self._adump.PAGE_SIZE)
 
-    command = ['pixeldump', '-a', start_address, '-' , self._adump.PAGE_SIZE,
+    command = ['pixeldump', '-a', start_address, '-', self._adump.PAGE_SIZE,
                page_count, 1]
     logging.info('Dump: %s', command)
     p = system_tools.SystemTools.RunInSubprocess(*command)
diff --git a/chameleond/utils/system_tools.py b/chameleond/utils/system_tools.py
index 0d7192a..7771c69 100644
--- a/chameleond/utils/system_tools.py
+++ b/chameleond/utils/system_tools.py
@@ -14,6 +14,7 @@
   _TOOL_PATHS = {
       'aplay': '/usr/bin/aplay',
       'arecord': '/usr/bin/arecord',
+      'avsync': '/usr/bin/avsync',
       'chameleond': '/etc/init.d/chameleond',
       'date': '/bin/date',
       'i2cdump': '/usr/local/sbin/i2cdump',
diff --git a/src/avsync.c b/src/avsync.c
new file mode 100644
index 0000000..805a2cb
--- /dev/null
+++ b/src/avsync.c
@@ -0,0 +1,117 @@
+// Copyright 2016 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.
+
+/* Audio Video Capturing synchronization utility
+ *
+ * This is a command-line tool running on chameleon board to monitor the
+ * changes of the audio page count and the video page count, and calcuate the
+ * time interval between the first audio/video data captured.
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+const int controller_addr = 0xff210000;
+const int controller_size = 0x10000;
+
+char* mem;
+
+inline int read_mem(int addr)
+{
+  return *(int*)(mem + (addr - controller_addr));
+}
+
+inline int audio_page_count()
+{
+  const int audio_regs_base = 0xff212000;
+  const int audio_reg_page_count = 0x14;
+
+  return read_mem(audio_regs_base + audio_reg_page_count);
+}
+
+inline int video_field_count()
+{
+  const int video_regs_base = 0xff210000;
+  const int video_reg_frame_count = 0x20;
+
+  return read_mem(video_regs_base + video_reg_frame_count);
+}
+
+int main()
+{
+  const int fd = open("/dev/mem", O_RDONLY | O_SYNC);
+  if (fd == -1) {
+    perror("open");
+    exit(1);
+  }
+
+  mem = mmap(NULL, controller_size, PROT_READ, MAP_SHARED, fd, controller_addr);
+  if (mem == MAP_FAILED) {
+    perror("mmap");
+    exit(1);
+  }
+
+  int last_audio_page_count = audio_page_count();
+  int last_video_frame_count = video_field_count();
+
+  struct timeval ta, tv;
+  bool done_audio = false, done_video = false;
+  time_t timeout = time(NULL) + 20;
+  while ((!done_audio || !done_video) && time(NULL) < timeout) {
+    if (!done_audio) {
+      const int current_audio_page_count = audio_page_count();
+      if (current_audio_page_count > last_audio_page_count) {
+        gettimeofday(&ta, NULL);
+        done_audio = true;
+      }
+      last_audio_page_count = current_audio_page_count;
+    }
+    /*
+     * In chameleond, VideoDumper will capture 1 frame when it selects a new
+     * input, so the change of the frame count from 0 to 1 may be originated
+     * from that frame, and the second captured frame is always the frame
+     * we care about.
+     */
+    if (!done_video) {
+      const int current_video_frame_count = video_field_count();
+      if (current_video_frame_count > last_video_frame_count &&
+          current_video_frame_count >= 2) {
+        gettimeofday(&tv, NULL);
+        done_video = true;
+      }
+      last_video_frame_count = current_video_frame_count;
+    }
+    usleep(100);
+  }
+
+  if (!done_audio || !done_video) {
+    return -1;
+  }
+
+  /*
+   * Because tv is the time when the second frame was captured, to estimate
+   * the time when the first frame was captured, we need to shift it by -1/60
+   * second.
+   */
+  const double diff = (tv.tv_sec - ta.tv_sec) +
+                      (tv.tv_usec - ta.tv_usec) * 1e-6 -
+                      1.0 / 60;
+
+  printf("%.8f\n", diff);
+
+  munmap(mem, controller_size);
+  close(fd);
+
+  return 0;
+}