• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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