1#!/usr/bin/env python3 2# 3# Copyright 2019 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import logging 18import os 19import pyaudio 20import wave 21 22from acts import context 23 24WAVE_FILE_TEMPLATE = 'recorded_audio_%s.wav' 25ADB_PATH = 'sdcard/Music/' 26ADB_FILE = 'rec.pcm' 27 28 29class AudioCaptureBase(object): 30 """Base class for Audio capture.""" 31 def __init__(self): 32 33 self.wave_file = os.path.join(self.log_path, WAVE_FILE_TEMPLATE) 34 self.file_dir = self.log_path 35 36 @property 37 def log_path(self): 38 """Returns current log path.""" 39 current_context = context.get_current_context() 40 full_out_dir = os.path.join(current_context.get_full_output_path(), 41 'AudioCapture') 42 43 os.makedirs(full_out_dir, exist_ok=True) 44 return full_out_dir 45 46 @property 47 def next_fileno(self): 48 counter = 0 49 while os.path.exists(self.wave_file % counter): 50 counter += 1 51 return counter 52 53 @property 54 def last_fileno(self): 55 return self.next_fileno - 1 56 57 @property 58 def get_last_record_duration_millis(self): 59 """Get duration of most recently recorded file. 60 61 Returns: 62 duration (float): duration of recorded file in milliseconds. 63 """ 64 latest_file_path = self.wave_file % self.last_fileno 65 print(latest_file_path) 66 with wave.open(latest_file_path, 'r') as f: 67 frames = f.getnframes() 68 rate = f.getframerate() 69 duration = (frames / float(rate)) * 1000 70 return duration 71 72 def write_record_file(self, audio_params, frames): 73 """Writes the recorded audio into the file. 74 75 Args: 76 audio_params: A dict with audio configuration. 77 frames: Recorded audio frames. 78 79 Returns: 80 file_name: wave file name. 81 """ 82 file_name = self.wave_file % self.next_fileno 83 logging.debug('writing to %s' % file_name) 84 wf = wave.open(file_name, 'wb') 85 wf.setnchannels(audio_params['channel']) 86 wf.setsampwidth(audio_params['sample_width']) 87 wf.setframerate(audio_params['sample_rate']) 88 wf.writeframes(frames) 89 wf.close() 90 return file_name 91 92 93class CaptureAudioOverAdb(AudioCaptureBase): 94 """Class to capture audio over android device which acts as the 95 a2dp sink or hfp client. This captures the digital audio and converts 96 to analog audio for post processing. 97 """ 98 def __init__(self, ad, audio_params): 99 """Initializes CaptureAudioOverAdb. 100 101 Args: 102 ad: An android device object. 103 audio_params: Dict containing audio record settings. 104 """ 105 super().__init__() 106 self._ad = ad 107 self.audio_params = audio_params 108 self.adb_path = None 109 110 def start(self): 111 """Start the audio capture over adb.""" 112 self.adb_path = os.path.join(ADB_PATH, ADB_FILE) 113 cmd = 'ap2f --usage 1 --start --duration {} --target {}'.format( 114 self.audio_params['duration'], 115 self.adb_path, 116 ) 117 return self._ad.adb.shell_nb(cmd) 118 119 def stop(self): 120 """Stops the audio capture and stores it in wave file. 121 122 Returns: 123 File name of the recorded file. 124 """ 125 cmd = '{} {}'.format(self.adb_path, self.file_dir) 126 self._ad.adb.pull(cmd) 127 self._ad.adb.shell('rm {}'.format(self.adb_path)) 128 return self._convert_pcm_to_wav() 129 130 def _convert_pcm_to_wav(self): 131 """Converts raw pcm data into wave file. 132 133 Returns: 134 file_path: Returns the file path of the converted file 135 (digital to analog). 136 """ 137 file_to_read = os.path.join(self.file_dir, ADB_FILE) 138 with open(file_to_read, 'rb') as pcm_file: 139 frames = pcm_file.read() 140 file_path = self.write_record_file(self.audio_params, frames) 141 return file_path 142 143 144class CaptureAudioOverLocal(AudioCaptureBase): 145 """Class to capture audio on local server using the audio input devices 146 such as iMic/AudioBox. This class mandates input deivce to be connected to 147 the machine. 148 """ 149 def __init__(self, audio_params): 150 """Initializes CaptureAudioOverLocal. 151 152 Args: 153 audio_params: Dict containing audio record settings. 154 """ 155 super().__init__() 156 self.audio_params = audio_params 157 self.channels = self.audio_params['channel'] 158 self.chunk = self.audio_params['chunk'] 159 self.sample_rate = self.audio_params['sample_rate'] 160 self.__input_device = None 161 self.audio = None 162 self.frames = [] 163 164 @property 165 def name(self): 166 return self.__input_device["name"] 167 168 def __get_input_device(self): 169 """Checks for the audio capture device.""" 170 if self.__input_device is None: 171 for i in range(self.audio.get_device_count()): 172 device_info = self.audio.get_device_info_by_index(i) 173 logging.debug('Device Information: {}'.format(device_info)) 174 if self.audio_params['input_device'] in device_info['name']: 175 self.__input_device = device_info 176 break 177 else: 178 raise DeviceNotFound( 179 'Audio Capture device {} not found.'.format( 180 self.audio_params['input_device'])) 181 return self.__input_device 182 183 def start(self, trim_beginning=0, trim_end=0): 184 """Starts audio recording on host machine. 185 186 Args: 187 trim_beginning: how many seconds to trim from the beginning 188 trim_end: how many seconds to trim from the end 189 """ 190 self.audio = pyaudio.PyAudio() 191 self.__input_device = self.__get_input_device() 192 stream = self.audio.open( 193 format=pyaudio.paInt16, 194 channels=self.channels, 195 rate=self.sample_rate, 196 input=True, 197 frames_per_buffer=self.chunk, 198 input_device_index=self.__input_device['index']) 199 b_chunks = trim_beginning * (self.sample_rate // self.chunk) 200 e_chunks = trim_end * (self.sample_rate // self.chunk) 201 total_chunks = self.sample_rate // self.chunk * self.audio_params[ 202 'duration'] 203 for i in range(total_chunks): 204 try: 205 data = stream.read(self.chunk, exception_on_overflow=False) 206 except IOError as ex: 207 logging.error('Cannot record audio: {}'.format(ex)) 208 return False 209 if b_chunks <= i < total_chunks - e_chunks: 210 self.frames.append(data) 211 212 stream.stop_stream() 213 stream.close() 214 215 def stop(self): 216 """Terminates the pulse audio instance. 217 218 Returns: 219 File name of the recorded audio file. 220 """ 221 self.audio.terminate() 222 frames = b''.join(self.frames) 223 return self.write_record_file(self.audio_params, frames) 224 225 226class DeviceNotFound(Exception): 227 """Raises exception if audio capture device is not found.""" 228