test_server: Add utils to detect continuous 0 from audio data
Add one handy function to detect continous 0 from chameleon. This
function will also save problem samples to .wav file. It can detect for
a very long time by chameleon stream server.
This CL also fixed the lint errors.
Example usage:
>>> ConnectCrosToLineIn()
>>> p.StartCapturingAudio(6)
(do someting on DUT to play
audio to headphone)
>>> DetectAudioValue0()
BUG=None
TEST=Test audio_AudioBasicHDMI and audio_AudioBasicHeadphone.
Test if we can detect the value by HDMI and audiojack interfaces.
Change-Id: I5b20e0cb039c083c823e6f4bb3b6d78e03c88ed5
Reviewed-on: https://chromium-review.googlesource.com/424256
Commit-Ready: Hsu Wei-Cheng <mojahsu@chromium.org>
Tested-by: Hsu Wei-Cheng <mojahsu@chromium.org>
Reviewed-by: Cheng-Yi Chiang <cychiang@chromium.org>
diff --git a/chameleond/drivers/fpga_tio.py b/chameleond/drivers/fpga_tio.py
index 1dc56bf..7d16e33 100644
--- a/chameleond/drivers/fpga_tio.py
+++ b/chameleond/drivers/fpga_tio.py
@@ -935,7 +935,7 @@
return self._audio_board is not None
@_AudioMethod(input_only=True)
- def StartCapturingAudio(self, port_id):
+ def StartCapturingAudio(self, port_id, has_file=True):
"""Starts capturing audio.
Refer to the docstring of StartPlayingEcho about the restriction of
@@ -943,10 +943,11 @@
Args:
port_id: The ID of the audio input port.
+ has_file: True for saving audio data to file. False otherwise.
"""
self._SelectInput(port_id)
logging.info('Start capturing audio from port #%d', port_id)
- self._flows[port_id].StartCapturingAudio()
+ self._flows[port_id].StartCapturingAudio(has_file)
@_AudioMethod(input_only=True)
def StopCapturingAudio(self, port_id):
@@ -962,6 +963,8 @@
of utils.audio.AudioDataFormat for detail.
Currently, the data format supported is
dict(file_type='raw', sample_format='S32_LE', channel=8, rate=48000)
+ If we assign parameter has_file=False in StartCapturingAudio, we will get
+ both empty string in path and format.
Raises:
DriverError: Input is selected to port other than port_id.
@@ -974,6 +977,10 @@
'The input is selected to %r not %r', self._selected_input, port_id)
path, data_format = self._flows[port_id].StopCapturingAudio()
logging.info('Stopped capturing audio from port #%d', port_id)
+ # If there is no path, set it to empty string. Because XMLRPC doesn't
+ # support None as return value.
+ if path is None and data_format is None:
+ return '', ''
return path, data_format
@_AudioMethod(output_only=True)
diff --git a/chameleond/interface.py b/chameleond/interface.py
index 5fbb171..b26b796 100644
--- a/chameleond/interface.py
+++ b/chameleond/interface.py
@@ -489,7 +489,7 @@
"""
raise NotImplementedError('DetectResolution')
- def StartCapturingAudio(self, port_id):
+ def StartCapturingAudio(self, port_id, has_file=True):
"""Starts capturing audio.
Refer to the docstring of StartPlayingEcho about the restriction of
@@ -497,6 +497,7 @@
Args:
port_id: The ID of the audio input port.
+ has_file: True for saving audio data to file. False otherwise.
"""
raise NotImplementedError('StartCapturingAudio')
diff --git a/chameleond/utils/audio_utils.py b/chameleond/utils/audio_utils.py
index aadd990..1324662 100644
--- a/chameleond/utils/audio_utils.py
+++ b/chameleond/utils/audio_utils.py
@@ -6,15 +6,12 @@
import logging
import os
import struct
-import tempfile
-import time
import chameleon_common # pylint: disable=W0611
from chameleond.utils import fpga
from chameleond.utils import ids
from chameleond.utils import mem
from chameleond.utils import memory_dumper
-from chameleond.utils import system_tools
class AudioCaptureManagerError(Exception):
@@ -52,12 +49,14 @@
"""Starts capturing audio.
Args:
- file_path: The target file for audio capturing.
+ file_path: The target file for audio capturing. None for no target file.
"""
self._file_path = file_path
self._adump.StartDumpingToMemory()
- self._mem_dumper = memory_dumper.MemoryDumper(file_path, self._adump)
- self._mem_dumper.start()
+ self._mem_dumper = None
+ if file_path:
+ self._mem_dumper = memory_dumper.MemoryDumper(file_path, self._adump)
+ self._mem_dumper.start()
logging.info('Started capturing audio.')
def StopCapturingAudio(self):
@@ -74,12 +73,13 @@
if not self.is_capturing:
raise AudioCaptureManagerError('Stop Capturing audio before Start')
- self._mem_dumper.Stop()
- self._mem_dumper.join()
- start_address, page_count = self._adump.StopDumpingToMemory()
+ if self._mem_dumper:
+ self._mem_dumper.Stop()
+ self._mem_dumper.join()
+ _, page_count = self._adump.StopDumpingToMemory()
logging.info('Stopped capturing audio.')
- if self._mem_dumper.exitcode:
+ if self._mem_dumper and self._mem_dumper.exitcode:
raise AudioCaptureManagerError(
'MemoryDumper was terminated unexpectedly.')
@@ -89,7 +89,8 @@
# Workaround for issue crbug.com/574683 where the last two pages should
# be neglected.
- self._TruncateFile(2)
+ if self._file_path:
+ self._TruncateFile(2)
return self._adump.audio_data_format_as_dict
@@ -105,7 +106,7 @@
file_size = os.path.getsize(self._file_path)
new_file_size = file_size - self._adump.PAGE_SIZE * pages
if new_file_size <= 0:
- raise AudioCaptureManagerError('Not enough audio data was captured.')
+ raise AudioCaptureManagerError('Not enough audio data was captured.')
with open(self._file_path, 'r+') as f:
f.truncate(new_file_size)
@@ -382,4 +383,3 @@
True if generator clock is required for source.
"""
return source != fpga.AudioSource.RX_I2S
-
diff --git a/chameleond/utils/codec_flow.py b/chameleond/utils/codec_flow.py
index bc89d23..774485c 100644
--- a/chameleond/utils/codec_flow.py
+++ b/chameleond/utils/codec_flow.py
@@ -140,14 +140,22 @@
"""
return self._audio_capture_manager.is_capturing
- def StartCapturingAudio(self):
- """Starts capturing audio."""
+ def StartCapturingAudio(self, has_file):
+ """Starts capturing audio.
+
+ Args:
+ has_file: True for saving audio data to file. False otherwise.
+ """
self._audio_codec.SelectInput(self._CODEC_INPUTS[self._port_id])
self._audio_route_manager.SetupRouteFromInputToDumper(self._port_id)
- self._recorded_file = tempfile.NamedTemporaryFile(
- prefix='audio_', suffix='.raw', delete=False)
- logging.info('Save captured audio to %s', self._recorded_file.name)
- self._audio_capture_manager.StartCapturingAudio(self._recorded_file.name)
+ self._recorded_file = None
+ file_path = None
+ if has_file:
+ self._recorded_file = tempfile.NamedTemporaryFile(
+ prefix='audio_', suffix='.raw', delete=False)
+ file_path = self._recorded_file.name
+ logging.info('Save captured audio to %s', file_path)
+ self._audio_capture_manager.StartCapturingAudio(file_path)
def StopCapturingAudio(self):
"""Stops capturing audio.
@@ -157,6 +165,8 @@
path: The path to the captured audio data.
format: The dict representation of AudioDataFormat. Refer to docstring
of utils.audio.AudioDataFormat for detail.
+ If we assign parameter has_file=False in StartCapturingAudio, we will get
+ both None in path and format.
Raises:
AudioCaptureManagerError: If captured time or page exceeds the limit.
@@ -165,7 +175,10 @@
data_format = self._audio_capture_manager.StopCapturingAudio()
self._audio_codec.SelectInput(codec.CodecInput.NONE)
self.ResetRoute()
- return self._recorded_file.name, data_format
+ if self._recorded_file:
+ return self._recorded_file.name, data_format
+ else:
+ return None, None
def ResetRoute(self):
"""Resets the audio route."""
diff --git a/chameleond/utils/input_flow.py b/chameleond/utils/input_flow.py
index c3d95d5..4d950f7 100644
--- a/chameleond/utils/input_flow.py
+++ b/chameleond/utils/input_flow.py
@@ -418,16 +418,24 @@
"""Is input flow capturing audio?"""
return self._audio_capture_manager.is_capturing
- def StartCapturingAudio(self):
- """Starts capturing audio."""
+ def StartCapturingAudio(self, has_file):
+ """Starts capturing audio.
+
+ Args:
+ has_file: True for saving audio data to file. False otherwise.
+ """
self._audio_route_manager.SetupRouteFromInputToDumper(self._input_id)
- self._audio_recorded_file = tempfile.NamedTemporaryFile(
- prefix='audio_', suffix='.raw', delete=False)
- logging.info('Save captured audio to %s', self._audio_recorded_file.name)
+ self._audio_recorded_file = None
+ file_path = None
+ if has_file:
+ self._audio_recorded_file = tempfile.NamedTemporaryFile(
+ prefix='audio_', suffix='.raw', delete=False)
+ file_path = self._audio_recorded_file.name
+ logging.info('Save captured audio to %s',
+ self._audio_recorded_file.name)
# Resets audio logic for issue crbug.com/623466.
self._ResetAudioLogic()
- self._audio_capture_manager.StartCapturingAudio(
- self._audio_recorded_file.name)
+ self._audio_capture_manager.StartCapturingAudio(file_path)
def StopCapturingAudio(self):
"""Stops capturing audio.
@@ -437,6 +445,8 @@
path: The path to the captured audio data.
format: The dict representation of AudioDataFormat. Refer to docstring
of utils.audio.AudioDataFormat for detail.
+ If we assign parameter has_file=False in StartCapturingAudio, we will get
+ both None in path and format.
Raises:
AudioCaptureManagerError: If captured time or page exceeds the limit.
@@ -444,7 +454,10 @@
"""
data_format = self._audio_capture_manager.StopCapturingAudio()
self.ResetRoute()
- return self._audio_recorded_file.name, data_format
+ if self._audio_recorded_file:
+ return self._audio_recorded_file.name, data_format
+ else:
+ return None, None
def ResetRoute(self):
"""Resets the audio route."""
diff --git a/chameleond/utils/usb_audio_flow.py b/chameleond/utils/usb_audio_flow.py
index 5adb180..166264c 100644
--- a/chameleond/utils/usb_audio_flow.py
+++ b/chameleond/utils/usb_audio_flow.py
@@ -185,8 +185,14 @@
self._captured_file_type = capture_configs.file_type
self._usb_ctrl.SetDriverCaptureConfigs(capture_configs)
- def StartCapturingAudio(self):
- """Starts recording audio data."""
+ def StartCapturingAudio(self, has_file):
+ """Starts capturing audio.
+
+ Args:
+ has_file: It is only can be True for USB audio.
+ """
+ if not has_file:
+ raise USBAudioFlowError('USB Audio only supports save to file')
self._supported_data_format = self._usb_ctrl.GetSupportedCaptureDataFormat()
self._supported_data_format.file_type = self._captured_file_type
params_list = self._GetAlsaUtilCommandArgs(self._supported_data_format)
diff --git a/client/audio/__init__.py b/client/audio/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/audio/__init__.py
diff --git a/client/audio/audio_value_detector.py b/client/audio/audio_value_detector.py
new file mode 100644
index 0000000..184a84b
--- /dev/null
+++ b/client/audio/audio_value_detector.py
@@ -0,0 +1,185 @@
+# 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.
+
+"""A class used to detect audio streamed values close to 0."""
+
+import logging
+import os
+import struct
+import time
+import wave
+
+from chameleon_stream_server_proxy import (ChameleonStreamServer,
+ RealtimeMode)
+
+
+class AudioValueDetector(object):
+ """The class detects if Chameleon captures continuous audio data close to 0.
+
+ Attributes:
+ _host: The host address of chameleon board.
+ _audio_frame_format: Audio sample format from chameleon.
+ _audio_frame_bytes: Total bytes per audio sample.
+ _continuous_0_per_channel: An array to record how many continuous 0s we've
+ gotten per channel
+ _do_detect_0_per_channel: An array to indicate the detect state per
+ channel. True for detecting continous 0s. False for detecting non 0.
+ _num_print_frames: Used to print the number of values of the first audio
+ frames.
+ Used to fine tune the margin.
+ """
+ def __init__(self, host):
+ """Creates an AudioValueDetector object.
+
+ Args:
+ host: The chameleon board's host address.
+ """
+ self._host = host
+ # Audio sample from chameleon are 32 bits per channel and it has 8 channels.
+ self._audio_frame_format = '<llllllll'
+ self._audio_frame_bytes = struct.calcsize(self._audio_frame_format)
+ self._continuous_0_per_channel = None
+ self._do_detect_0_per_channel = None
+ self._num_print_frames = 0
+
+ def SaveChameleonAudioDataToWav(self, directory, data, file_name):
+ """Save audio data from chameleon to a wave file.
+
+ This function will use default parameters for the chameleon audio data.
+
+ Args:
+ directory: Save file in which directory.
+ data: audio data.
+ file_name: The file name of the wave file.
+ """
+ num_channels = 8
+ sampwidth = 4
+ framerate = 48000
+ comptype = "NONE"
+ compname = "not compressed"
+ file_path = '%s/%s' % (directory, file_name)
+ logging.warn('Save to %s', file_path)
+ wav_file = wave.open(file_path, 'w')
+ wav_file.setparams((num_channels, sampwidth, framerate, len(data), comptype,
+ compname))
+ wav_file.writeframes(data)
+ wav_file.close()
+
+ def _DetectAudioDataValues(self, channels, continuous_samples, data, margin):
+ """Detect if we get continuous 0s from chameleon audio data.
+
+ Args:
+ channels: Array of audio channels we want to check.
+ continuous_samples: When continuous_samples samples are closed to 0,
+ do detect 0 event.
+ data: A page of audio data from chameleon.
+ margin: Used to decide if the value is closed to 0. Maximum value is 1.
+
+ Returns:
+ Can save file or not. Save file when detecting continuous 0 or detecting
+ non-zero after continuous 0.
+ """
+ should_save_file = False
+
+ # Detect audio data values sample by sample.
+ offset = 0
+ while offset != len(data):
+ audio_sample = struct.unpack_from(self._audio_frame_format, data, offset)
+ offset = offset + self._audio_frame_bytes
+ for index, channel in enumerate(channels):
+ value = float(abs(audio_sample[channel]))/(1 << 31)
+ if self._num_print_frames:
+ logging.info('Value of channel %d is %f', channel, value)
+ # Value is close to 0.
+ if value < margin:
+ self._continuous_0_per_channel[index] += 1
+ # We've detected continuous 0s on this channel before. This sample is
+ # in the same continous 0s state.
+ if not self._do_detect_0_per_channel[index]:
+ continue
+ if self._continuous_0_per_channel[index] >= continuous_samples:
+ logging.warn('Detected continuous %d 0s of channel %d',
+ self._continuous_0_per_channel[index], channel)
+ self._do_detect_0_per_channel[index] = False
+ should_save_file = True
+ else:
+ # Value is not close to 0.
+ if not self._do_detect_0_per_channel[index]:
+ # This if section means we get non-0 after continuous 0s were
+ # detected.
+ logging.warn('Continuous %d 0s of channel %d',
+ self._continuous_0_per_channel[index], channel)
+ self._do_detect_0_per_channel[index] = True
+ should_save_file = True
+ # Reset number of continuous 0s when we detect a non-0 value.
+ self._continuous_0_per_channel[index] = 0
+ if self._num_print_frames:
+ self._num_print_frames -= 1
+ return should_save_file
+
+ def Detect(self, channels, margin, continuous_samples, duration, dump_frames):
+ """Detects if Chameleon captures continuous audio data close to 0.
+
+ This function will get the audio streaming data from stream server and will
+ check if the audio data is close to 0 by the margin parameter.
+ -margin < value < margin will be considered to be close to 0.
+ If there are continuous audio samples close to 0 in the streamed data,
+ test_server will log it and save the audio data to a wav file.
+
+ Args:
+ channels: Array of audio channels we want to check.
+ E.g. [0, 1] means we only care about channel 0 and channel 1.
+ margin: Used to decide if the value is closed to 0. Maximum value is 1.
+ continuous_samples: When continuous_samples samples are closed to 0,
+ trigger event.
+ duration: The duration of monitoring in seconds.
+ dump_frames: When event happens, how many audio frames we want to save to
+ file.
+ """
+ # Create a new directory for storing audio files.
+ directory = 'detect0_%s' % time.strftime('%Y%m%d%H%M%S', time.localtime())
+ if not os.path.exists(directory):
+ os.mkdir(directory)
+
+ dump_bytes = self._audio_frame_bytes * dump_frames
+
+ self._num_print_frames = 10
+ self._continuous_0_per_channel = [0] * len(channels)
+ self._do_detect_0_per_channel = [True] * len(channels)
+ audio_data = ''
+
+ stream = ChameleonStreamServer(self._host)
+ stream.connect()
+ stream.reset_audio_session()
+ stream.dump_realtime_audio_page(RealtimeMode.BestEffort)
+ start_time = time.time()
+ logging.info('Start to detect continuous 0s.')
+ logging.info('Channels=%r, margin=%f, continuous_samples=%d, '
+ 'duration=%d seconds, dump_frames=%d.', channels, margin,
+ continuous_samples, duration, dump_frames)
+ while True:
+ audio_page = stream.receive_realtime_audio_page()
+ if not audio_page:
+ logging.warn('No audio page, there may be a socket errror.')
+ break
+ # We've checked None before, so we can disable the false alarm.
+ (page_count, data) = audio_page # pylint: disable=unpacking-non-sequence
+ audio_data += data
+
+ # Only keep needed volume of data for saving memory usage.
+ audio_data = audio_data[-dump_bytes:]
+
+ should_save_file = self._DetectAudioDataValues(channels,
+ continuous_samples, data,
+ margin)
+ if should_save_file:
+ file_name = 'audio_%d.wav' % page_count
+ self.SaveChameleonAudioDataToWav(directory, audio_data, file_name)
+ audio_data = ''
+
+ current_time = time.time()
+ if current_time - start_time > duration:
+ stream.stop_dump_realtime_audio_page()
+ logging.warn('Timeout stop detect.')
+ break
diff --git a/client/chameleon_stream_server_proxy.py b/client/chameleon_stream_server_proxy.py
new file mode 100644
index 0000000..82a1c21
--- /dev/null
+++ b/client/chameleon_stream_server_proxy.py
@@ -0,0 +1,553 @@
+# 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.
+
+"""This module provides the utilities for chameleon streaming server usage.
+
+Sample Code for dumping real-time video frames:
+ stream = ChameleonStreamServer(IP)
+ stream.reset_video_session()
+
+ chameleon_proxy.StartCapturingVideo(port)
+ stream.dump_realtime_video_frame(False, RealtimeMode.BestEffort)
+ while True:
+ video_frame = stream.receive_realtime_video_frame()
+ if not video_frame:
+ break
+ (frame_number, width, height, channel, data) = video_frame
+ image = Image.fromstring('RGB', (width, height), data)
+ image.save('%d.bmp' % frame_number)
+
+Sample Code for dumping real-time audio pages:
+ stream = ChameleonStreamServer(IP)
+ stream.reset_audio_session()
+
+ chameleon_proxy.StartCapturingAudio(port)
+ stream.dump_realtime_audio_page(RealtimeMode.BestEffort)
+ f = open('audio.raw', 'w')
+ while True:
+ audio_page = stream.receive_realtime_audio_page()
+ if not audio_page:
+ break
+ (page_count, data) = audio_page
+ f.write(data)
+"""
+
+import collections
+import logging
+import socket
+from struct import calcsize, pack, unpack
+
+
+CHAMELEON_STREAM_SERVER_PORT = 9994
+SUPPORT_MAJOR_VERSION = 1
+SUPPORT_MINOR_VERSION = 0
+
+
+class StreamServerVersionError(Exception):
+ """Version is not compatible between client and server."""
+ pass
+
+
+class ErrorCode(object):
+ """Error codes of response from the stream server."""
+ OK = 0
+ NON_SUPPORT_COMMAND = 1
+ ARGUMENT = 2
+ REAL_TIME_STREAM_EXISTS = 3
+ VIDEO_MEMORY_OVERFLOW_STOP = 4
+ VIDEO_MEMORY_OVERFLOW_DROP = 5
+ AUDIO_MEMORY_OVERFLOW_STOP = 6
+ AUDIO_MEMORY_OVERFLOW_DROP = 7
+ MEMORY_ALLOC_FAIL = 8
+
+
+class RealtimeMode(object):
+ """Realtime mode of dumping data."""
+ # Stop dump when memory overflow
+ StopWhenOverflow = 1
+
+ # Drop data when memory overflow
+ BestEffort = 2
+
+ # Strings used for logging.
+ LogStrings = ['None', 'Stop when overflow', 'Best effort']
+
+
+class ChameleonStreamServer(object):
+ """This class provides easy-to-use APIs to access the stream server."""
+
+ # Main message types.
+ _REQUEST_TYPE = 0
+ _RESPONSE_TYPE = 1
+ _DATA_TYPE = 2
+
+ # uint16 type, uint16 error_code, uint32 length.
+ packet_head_struct = '!HHL'
+
+ # Message types.
+ Message = collections.namedtuple('Message', ['type',
+ 'request_struct',
+ 'response_struct',
+ 'data_struct'])
+ _RESET_MSG = Message(0, None, None, None)
+ # Response: uint8 major, uint8 minor.
+ _GET_VERSION_MSG = Message(1, None, '!BB', None)
+ # Request: unt16 screen_width, uint16 screen_height.
+ _CONFIG_VIDEO_STREAM_MSG = Message(2, '!HH', None, None)
+ # Request: uint8 shrink_width, uint8 shrink_height.
+ _CONFIG_SHRINK_VIDEO_STREAM_MSG = Message(3, '!BB', None, None)
+ # Request: uint32 memory_address1, uint32 memory_address2,
+ # uint16 number_of_frames.
+ # Data: uint32 frame_number, uint16 width, uint16 height, uint8 channel,
+ # uint8 padding[3]
+ _DUMP_VIDEO_FRAME_MSG = Message(4, '!LLH', None, '!LHHBBBB')
+ # Request: uint8 is_dual, uint8 mode.
+ # Data: uint32 frame_number, uint16 width, uint16 height, uint8 channel,
+ # uint8 padding[3]
+ _DUMP_REAL_TIME_VIDEO_FRAME_MSG = Message(5, '!BB', None, '!LHHBBBB')
+ _STOP_DUMP_VIDEO_FRAME_MSG = Message(6, None, None, None)
+ # Request: uint8 mode.
+ # Data: uint32 page_count.
+ _DUMP_REAL_TIME_AUDIO_PAGE = Message(7, '!B', None, '!L')
+ _STOP_DUMP_AUDIO_PAGE = Message(8, None, None, None)
+
+ _PACKET_HEAD_SIZE = 8
+
+ def __init__(self, hostname, port=CHAMELEON_STREAM_SERVER_PORT):
+ """Constructs a ChameleonStreamServer.
+
+ Args:
+ hostname: Hostname of stream server.
+ port: Port number the stream server is listening on.
+ """
+ self._video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._hostname = hostname
+ self._port = port
+ # Used for non-realtime dump video frames.
+ self._remain_frame_count = 0
+ self._is_realtime_video = False
+ self._is_realtime_audio = False
+
+ def _get_request_type(self, message):
+ """Get the request type of the message.
+
+ Args:
+ message: Message namedtuple.
+
+ Returns:
+ Request message type.
+ """
+ return (self._REQUEST_TYPE << 8) | message.type
+
+ def _get_response_type(self, message):
+ """Get the response type of the message.
+
+ Args:
+ message: Message namedtuple.
+
+ Returns:
+ Response message type.
+ """
+ return (self._RESPONSE_TYPE << 8) | message.type
+
+ def _is_data_type(self, message_type):
+ """Check if the message type is data type.
+
+ Args:
+ message_type: Message type
+
+ Returns:
+ Non 0 if the message is data type. otherwise 0.
+ """
+ return (self._DATA_TYPE << 8) & message_type
+
+ def _receive_whole_packet(self, sock):
+ """Receive one whole packet, contains packet head and content.
+
+ Args:
+ sock: Which socket to be used.
+
+ Returns:
+ A tuple with 4 elements: message_type, error code, length and content.
+
+ Raises:
+ ValueError if we can't receive data from server.
+ """
+ # receive packet header
+ data = sock.recv(self._PACKET_HEAD_SIZE)
+ if not data:
+ raise ValueError('Receive no data from server')
+
+ while len(data) != self._PACKET_HEAD_SIZE:
+ remain_length = self._PACKET_HEAD_SIZE - len(data)
+ recv_content = sock.recv(remain_length)
+ data += recv_content
+
+ message_type, error_code, length = unpack(self.packet_head_struct, data)
+
+ # receive content
+ content = ''
+ remain_length = length
+ while remain_length:
+ recv_content = sock.recv(remain_length)
+ if not recv_content:
+ raise ValueError('Receive no data from server')
+ remain_length -= len(recv_content)
+ content += recv_content
+
+ if error_code != ErrorCode.OK:
+ logging.warn('Receive error code %d, %r', error_code, content)
+
+ return (message_type, error_code, length, content)
+
+ def _send_and_receive(self, packet, sock, check_error=True):
+ """Send packet to server and receive response from server.
+
+ Args:
+ packet: The packet to be sent.
+ sock: Which socket to be used.
+ check_error: Check the error code. If this is True, this function will
+ check the error code from response and raise exception if the error
+ code is not OK.
+
+ Returns:
+ The response packet from server. A tuple with 4 elements contains
+ message_type, error code, length and content.
+
+ Raises:
+ ValueError if check_error and error code is not OK.
+ """
+ sock.send(packet)
+ packet = self._receive_whole_packet(sock)
+ if packet and check_error:
+ (_, error_code, _, _) = packet
+ if error_code != ErrorCode.OK:
+ raise ValueError('Error code is not OK')
+
+ return packet
+
+ def _generate_request_packet(self, message, *args):
+ """Generate whole request packet with parameters.
+
+ Args:
+ message: Message namedtuple.
+ *args: Packet contents.
+
+ Returns:
+ The whole request packet content.
+ """
+ if message.request_struct:
+ content = pack(message.request_struct, *args)
+ else:
+ content = ''
+
+ # Create header.
+ head = pack(self.packet_head_struct,
+ self._get_request_type(message),
+ ErrorCode.OK, len(content))
+
+ return head + content
+
+ def _receive_video_frame(self):
+ """Receive one video frame from server.
+
+ This function assumes it only can receive video frame data packet
+ from server. Error code will indicate success or not.
+
+ Returns:
+ If error code is OK, a decoded values will be stored in a tuple
+ (error_code, frame number, width, height, channel, data).
+ If error code is not OK, it will return a tuple (error code, content). The
+ content is the error message from server.
+
+ Raises:
+ ValueError if packet is not data packet.
+ """
+ (message, error_code, _, content) = self._receive_whole_packet(
+ self._video_sock)
+ if error_code != ErrorCode.OK:
+ return (error_code, content)
+
+ if not self._is_data_type(message):
+ raise ValueError('Message is not data')
+
+ video_frame_head_size = calcsize(self._DUMP_VIDEO_FRAME_MSG.data_struct)
+ frame_number, width, height, channel, _, _, _ = unpack(
+ self._DUMP_VIDEO_FRAME_MSG.data_struct, content[:video_frame_head_size])
+ data = content[video_frame_head_size:]
+ return (error_code, frame_number, width, height, channel, data)
+
+ def _get_version(self):
+ """Get the version of the server.
+
+ Returns:
+ A tuple with Major and Minor number of the server.
+ """
+ packet = self._generate_request_packet(self._GET_VERSION_MSG)
+ (_, _, _, content) = self._send_and_receive(packet, self._video_sock)
+ return unpack(self._GET_VERSION_MSG.response_struct, content)
+
+ def _check_version(self):
+ """Check if this client is compatible with the server.
+
+ The major number must be the same and the minor number of the server
+ must larger then the client's.
+
+ Returns:
+ Compatible or not
+ """
+ (major, minor) = self._get_version()
+ logging.debug('Major %d, minor %d', major, minor)
+ return major == SUPPORT_MAJOR_VERSION and minor >= SUPPORT_MINOR_VERSION
+
+ def connect(self):
+ """Connect to the server and check the compatibility.
+
+ Raises:
+ StreamServerVersionError if client is not compitable with server.
+ """
+ server_address = (self._hostname, self._port)
+ logging.info('connecting to %s:%s', self._hostname, self._port)
+ self._video_sock.connect(server_address)
+ self._audio_sock.connect(server_address)
+ if not self._check_version():
+ raise StreamServerVersionError()
+
+ def reset_video_session(self):
+ """Reset the video session."""
+ logging.info('Reset session')
+ packet = self._generate_request_packet(self._RESET_MSG)
+ self._send_and_receive(packet, self._video_sock)
+
+ def reset_audio_session(self):
+ """Reset the audio session.
+
+ For audio, we don't need to reset any thing.
+ """
+ pass
+
+ def config_video_stream(self, width, height):
+ """Configure the properties of the non-realtime video stream.
+
+ Args:
+ width: The screen width of the video frame by pixel per channel.
+ height: The screen height of the video frame by pixel per channel.
+ """
+ logging.info('Config video, width %d, height %d', width, height)
+ packet = self._generate_request_packet(self._CONFIG_VIDEO_STREAM_MSG, width,
+ height)
+ self._send_and_receive(packet, self._video_sock)
+
+ def config_shrink_video_stream(self, shrink_width, shrink_height):
+ """Configure the shrink operation of the video frame dump.
+
+ Args:
+ shrink_width: Shrink (shrink_width+1) pixels to 1 pixel when do video
+ dump. 0 means no shrink.
+ shrink_height: Shrink (shrink_height+1) to 1 height when do video dump.
+ 0 means no shrink.
+ """
+ logging.info('Config shrink video, shirnk_width %d, shrink_height %d',
+ shrink_width, shrink_height)
+ packet = self._generate_request_packet(self._CONFIG_VIDEO_STREAM_MSG,
+ shrink_width, shrink_height)
+ self._send_and_receive(packet, self._video_sock)
+
+ def dump_video_frame(self, count, address1, address2):
+ """Ask server to dump video frames.
+
+ User must use receive_video_frame() to receive video frames after
+ calling this API.
+
+ Sample Code:
+ address = chameleon_proxy.GetCapturedFrameAddresses(0)
+ count = chameleon_proxy.GetCapturedFrameCount()
+ server.dump_video_frame(count, int(address), 0)
+ while True:
+ video_frame = server.receive_video_frame()
+ if not video_frame:
+ break
+ (frame_number, width, height, channel, data) = video_frame
+ image = Image.fromstring('RGB', (width, height), data)
+ image.save('%s.bmp' % frame_number)
+
+ Args:
+ count: Specify number of video frames.
+ address1: Dump memory address1.
+ address2: Dump memory address2. If it is 0. It means we only dump from
+ address1.
+ """
+ logging.info('dump video frame count %d, address1 0x%x, address2 0x%x',
+ count, address1, address2)
+ packet = self._generate_request_packet(self._DUMP_VIDEO_FRAME_MSG, address1,
+ address2, count)
+ self._send_and_receive(packet, self._video_sock)
+ self._remain_frame_count = count
+
+ def dump_realtime_video_frame(self, is_dual, mode):
+ """Ask server to dump realtime video frames.
+
+ User must use receive_realtime_video_frame() to receive video frames
+ after calling this API.
+
+ Sample Code:
+ server.dump_realtime_video_frame(False,
+ RealtimeMode.StopWhenOverflow)
+ while True:
+ video_frame = server.receive_realtime_video_frame()
+ if not video_frame:
+ break
+ (frame_number, width, height, channel, data) = video_frame
+ image = Image.fromstring('RGB', (width, height), data)
+ image.save('%s.bmp' % frame_number)
+
+ Args:
+ is_dual: False: means only dump from channel1, True: means dump from dual
+ channels.
+ mode: The values of RealtimeMode.
+ """
+ logging.info('dump realtime video frame is_dual %d, mode %s', is_dual,
+ RealtimeMode.LogStrings[mode])
+ packet = self._generate_request_packet(self._DUMP_REAL_TIME_VIDEO_FRAME_MSG,
+ is_dual, mode)
+ self._send_and_receive(packet, self._video_sock)
+ self._is_realtime_video = True
+
+ def receive_video_frame(self):
+ """Receive one video frame from server after calling dump_video_frame().
+
+ This function assumes it only can receive video frame data packet
+ from server. Error code will indicate success or not.
+
+ Returns:
+ A tuple with video frame information.
+ (frame number, width, height, channel, data), None if error happens.
+ """
+ if not self._remain_frame_count:
+ return None
+ self._remain_frame_count -= 1
+ frame_info = self._receive_video_frame()
+ if frame_info[0] != ErrorCode.OK:
+ self._remain_frame_count = 0
+ return None
+ return frame_info[1:]
+
+ def receive_realtime_video_frame(self):
+ """Receive one video frame from server.
+
+ After calling dump_realtime_video_frame(). The video frame may be dropped if
+ we use BestEffort mode. We can detect it by the frame number.
+
+ This function assumes it only can receive video frame data packet
+ from server. Error code will indicate success or not.
+
+ Returns:
+ A tuple with video frame information.
+ (frame number, width, height, channel, data), None if error happens or
+ no more frames.
+ """
+ if not self._is_realtime_video:
+ return None
+
+ frame_info = self._receive_video_frame()
+ # We can still receive video frame for drop case.
+ while frame_info[0] == ErrorCode.VIDEO_MEMORY_OVERFLOW_DROP:
+ frame_info = self._receive_video_frame()
+
+ if frame_info[0] != ErrorCode.OK:
+ return None
+
+ return frame_info[1:]
+
+ def stop_dump_realtime_video_frame(self):
+ """Ask server to stop dump realtime video frame."""
+ if not self._is_realtime_video:
+ return
+ packet = self._generate_request_packet(self._STOP_DUMP_VIDEO_FRAME_MSG)
+ self._video_sock.send(packet)
+ # Drop video frames until receive _StopDumpVideoFrame response.
+ while True:
+ (message, _, _, _) = self._receive_whole_packet(self._video_sock)
+ if message == self._get_response_type(self._STOP_DUMP_VIDEO_FRAME_MSG):
+ break
+ self._is_realtime_video = False
+
+ def dump_realtime_audio_page(self, mode):
+ """Ask server to dump realtime audio pages.
+
+ User must use receive_realtime_audio_page() to receive audio pages
+ after calling this API.
+
+ Sample Code for BestEffort:
+ server.dump_realtime_audio_page(RealtimeMode.kBestEffort)
+ f = open('audio.raw'), 'w')
+ while True:
+ audio_page = server.receive_realtime_audio_page()
+ if audio_page:
+ break
+ (page_count, data) = audio_page
+ f.write(data)
+
+ Args:
+ mode: The values of RealtimeMode.
+
+ Raises:
+ ValueError if error code from response is not OK.
+ """
+ logging.info('dump realtime audio page mode %s',
+ RealtimeMode.LogStrings[mode])
+ packet = self._generate_request_packet(self._DUMP_REAL_TIME_AUDIO_PAGE,
+ mode)
+ self._send_and_receive(packet, self._audio_sock)
+ self._is_realtime_audio = True
+
+ def receive_realtime_audio_page(self):
+ """Receive one audio page from server.
+
+ After calling dump_realtime_audio_page(). The behavior is the same as
+ receive_realtime_video_frame(). The audio page may be dropped if we use
+ BestEffort mode. We can detect it by the page count.
+
+ This function assumes it can receive audio page data packet
+ from server. Error code will indicate success or not.
+
+ Returns:
+ A tuple with audio page information: (page count, data) None if error
+ happens or no more frames.
+
+ Rraises:
+ ValueError if packet is not data packet.
+ """
+ if not self._is_realtime_audio:
+ return None
+ (message, error_code, _, content) = self._receive_whole_packet(
+ self._audio_sock)
+ # We can still receive audio page for drop case.
+ while error_code == ErrorCode.AUDIO_MEMORY_OVERFLOW_DROP:
+ (message, error_code, _, content) = self._receive_whole_packet(
+ self._audio_sock)
+
+ if error_code != ErrorCode.OK:
+ return None
+ if not self._is_data_type(message):
+ raise ValueError('Message is not data')
+
+ page_count = unpack(self._DUMP_REAL_TIME_AUDIO_PAGE.data_struct,
+ content[:4])[0]
+ data = content[4:]
+ return (page_count, data)
+
+ def stop_dump_realtime_audio_page(self):
+ """Ask server to stop dump realtime audio page."""
+ if not self._is_realtime_audio:
+ return
+ packet = self._generate_request_packet(self._STOP_DUMP_AUDIO_PAGE)
+ self._audio_sock.send(packet)
+ # Drop audio pages until receive _StopDumpAudioPage response.
+ while True:
+ (message, _, _, _) = self._receive_whole_packet(self._audio_sock)
+ if message == self._get_response_type(self._STOP_DUMP_AUDIO_PAGE):
+ break
+ self._is_realtime_audio = False
diff --git a/utils/test_server b/client/test_server.py
similarity index 62%
rename from utils/test_server
rename to client/test_server.py
index c775112..2fadacc 100755
--- a/utils/test_server
+++ b/client/test_server.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python2
# Copyright 2015 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.
@@ -14,6 +14,8 @@
import subprocess
import xmlrpclib
+from audio.audio_value_detector import AudioValueDetector
+
def ShowMessages(proxy):
"""Shows the messages for usage.
@@ -40,20 +42,50 @@
p.StartCapturingAudio(%d) to capture from LineIn.
p.StopCapturingAudio(%d) to stop capturing from LineIn.
p.Plug(%d) to plug HDMI.
- p.UnPlug(%d) to unplug HDMI.''',
- '\n '.join(port_messages), linein_port, linein_port,
- hdmi_port, hdmi_port)
+ p.Unplug(%d) to unplug HDMI.''', '\n '.join(port_messages),
+ linein_port, linein_port, hdmi_port, hdmi_port)
-def StartInteractiveShell(p, options):
+def DetectAudioValue0(channels=None, margin=0.01, continuous_samples=5,
+ duration=3600, dump_samples=48000):
+ """Detects if Chameleon captures continuous audio data close to 0.
+
+ This function will get the audio streaming data from stream server and will
+ check if the audio data is close to 0 by the margin parameter.
+ -margin < value < margin will be considered to be close to 0.
+ If there are continuous audio samples close to 0 in the streamed data,
+ test_server will log it and save the audio data to a wav file.
+
+ E.g.
+ >>> ConnectCrosToLineIn()
+ >>> p.StartCapturingAudio(6, False)
+ >>> DetectAudioValue0(duration=24*3600, margin=0.001)
+
+ Args:
+ channels: Array of audio channels we want to check.
+ E.g. [0, 1] means we only care about channel 0 and channel 1.
+ margin: Used to decide if the value is closed to 0. Maximum value is 1.
+ continuous_samples: When continuous_samples samples are closed to 0, trigger
+ event.
+ duration: The duration of monitoring in seconds.
+ dump_samples: When event happens, how many audio samples we want to
+ save to file.
+ """
+ if not channels:
+ channels = [0, 1]
+ detecter = AudioValueDetector(options.host) # pylint: disable=undefined-variable
+ detecter.Detect(channels, margin, continuous_samples, duration, dump_samples)
+ return True
+
+
+def StartInteractiveShell(p, options): # pylint: disable=unused-argument
"""Starts an interactive shell.
Args:
p: The xmlrpclib.ServerProxy to chameleond.
options: The namespace from argparse.
-
"""
- vars = globals()
+ vars = globals() # pylint: disable=redefined-builtin
vars.update(locals())
readline.set_completer(rlcompleter.Completer(vars).complete)
readline.parse_and_bind("tab: complete")
@@ -64,8 +96,8 @@
def ParseArgs():
"""Parses the arguments.
- Returns: the namespace containing parsed arguments.
-
+ Returns:
+ the namespace containing parsed arguments.
"""
parser = argparse.ArgumentParser(
description='Connect to Chameleond and use interactive shell.',
@@ -94,12 +126,11 @@
Args:
remote_path: The file to copy from Chameleon host.
-
"""
basename = os.path.basename(remote_path)
# options is already in the namespace.
subprocess.check_call(
- ['scp', 'root@%s:%s' % (options.host, remote_path), basename])
+ ['scp', 'root@%s:%s' % (options.host, remote_path), basename]) # pylint: disable=undefined-variable
subprocess.check_call(
['sox', '-b', '32', '-r', '48000', '-c', '8', '-e', 'signed',
basename, '-c', '2', basename + '.wav'])
@@ -107,8 +138,8 @@
def ConnectCrosToLineIn():
"""Connects a audio bus path from Cros headphone to Chameleon LineIn."""
- p.AudioBoardConnect(1, 'Cros device headphone')
- p.AudioBoardConnect(1, 'Chameleon FPGA line-in')
+ p.AudioBoardConnect(1, 'Cros device headphone') # pylint: disable=undefined-variable
+ p.AudioBoardConnect(1, 'Chameleon FPGA line-in') # pylint: disable=undefined-variable
def Main():