blob: 7f32030c9e808c762c3c585a74fd6b02abc32ba3 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2019 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
import pyaudio
import wave
from acts import context
from acts import utils
WAVE_FILE_TEMPLATE = 'recorded_audio_%s.wav'
ADB_PATH = 'sdcard/Music/'
ADB_FILE = 'rec.pcm'
class AudioCaptureBase(object):
"""Base class for Audio capture."""
def __init__(self):
self.wave_file = os.path.join(self.log_path, WAVE_FILE_TEMPLATE)
self.file_dir = self.log_path
@property
def log_path(self):
"""Returns current log path."""
current_context = context.get_current_context()
full_out_dir = os.path.join(current_context.get_full_output_path(),
'AudioCapture')
utils.create_dir(full_out_dir)
return full_out_dir
@property
def next_fileno(self):
counter = 0
while os.path.exists(self.wave_file % counter):
counter += 1
return counter
@property
def last_fileno(self):
return self.next_fileno - 1
def write_record_file(self, audio_params, frames):
"""Writes the recorded audio into the file.
Args:
audio_params: A dict with audio configuration.
frames: Recorded audio frames.
Returns:
file_name: wave file name.
"""
file_name = self.wave_file % self.next_fileno
logging.debug('writing to %s' % file_name)
wf = wave.open(file_name, 'wb')
wf.setnchannels(audio_params['channel'])
wf.setsampwidth(audio_params['sample_width'])
wf.setframerate(audio_params['sample_rate'])
wf.writeframes(frames)
wf.close()
return file_name
class CaptureAudioOverAdb(AudioCaptureBase):
"""Class to capture audio over android device which acts as the
a2dp sink or hfp client. This captures the digital audio and converts
to analog audio for post processing.
"""
def __init__(self, ad, audio_params):
"""Initializes CaptureAudioOverAdb.
Args:
ad: An android device object.
audio_params: Dict containing audio record settings.
"""
super().__init__()
self._ad = ad
self.audio_params = audio_params
self.adb_path = None
def start(self):
"""Start the audio capture over adb."""
self.adb_path = os.path.join(ADB_PATH, ADB_FILE)
cmd = 'ap2f --usage 1 --start --duration {} --target {}'.format(
self.audio_params['duration'], self.adb_path,
)
self._ad.adb.shell(cmd)
def stop(self):
"""Stops the audio capture and stores it in wave file.
Returns:
File name of the recorded file.
"""
cmd = '{} {}'.format(self.adb_path, self.file_dir)
self._ad.adb.pull(cmd)
self._ad.adb.shell('rm {}'.format(self.adb_path))
return self._convert_pcm_to_wav()
def _convert_pcm_to_wav(self):
"""Converts raw pcm data into wave file.
Returns:
file_path: Returns the file path of the converted file
(digital to analog).
"""
file_to_read = os.path.join(self.file_dir, ADB_FILE)
with open(file_to_read, 'rb') as pcm_file:
frames = pcm_file.read()
file_path = self.write_record_file(self.audio_params, frames)
return file_path
class CaptureAudioOverLocal(AudioCaptureBase):
"""Class to capture audio on local server using the audio input devices
such as iMic/AudioBox. This class mandates input deivce to be connected to
the machine.
"""
def __init__(self, audio_params):
"""Initializes CaptureAudioOverLocal.
Args:
audio_params: Dict containing audio record settings.
"""
super().__init__()
self.audio_params = audio_params
self.channels = self.audio_params['channel']
self.chunk = self.audio_params['chunk']
self.sample_rate = self.audio_params['sample_rate']
self.__input_device = None
self.audio = None
self.frames = []
@property
def name(self):
return self.__input_device["name"]
def __get_input_device(self):
"""Checks for the audio capture device."""
if self.__input_device is None:
for i in range(self.audio.get_device_count()):
device_info = self.audio.get_device_info_by_index(i)
logging.debug('Device Information: {}'.format(device_info))
if self.audio_params['input_device'] in device_info['name']:
self.__input_device = device_info
break
else:
raise DeviceNotFound(
'Audio Capture device {} not found.'.format(
self.audio_params['input_device']))
return self.__input_device
def start(self, trim_beginning=0, trim_end=0):
"""Starts audio recording on host machine.
Args:
trim_beginning: how many seconds to trim from the beginning
trim_end: how many seconds to trim from the end
"""
self.audio = pyaudio.PyAudio()
self.__input_device = self.__get_input_device()
stream = self.audio.open(
format=pyaudio.paInt16,
channels=self.channels,
rate=self.sample_rate,
input=True,
frames_per_buffer=self.chunk,
input_device_index=self.__input_device['index'])
b_chunks = trim_beginning * (self.sample_rate // self.chunk)
e_chunks = trim_end * (self.sample_rate // self.chunk)
total_chunks = self.sample_rate // self.chunk * self.audio_params[
'duration']
for i in range(total_chunks):
try:
data = stream.read(self.chunk, exception_on_overflow=False)
except IOError as ex:
logging.error('Cannot record audio: {}'.format(ex))
return False
if b_chunks <= i < total_chunks - e_chunks:
self.frames.append(data)
stream.stop_stream()
stream.close()
def stop(self):
"""Terminates the pulse audio instance.
Returns:
File name of the recorded audio file.
"""
self.audio.terminate()
frames = b''.join(self.frames)
return self.write_record_file(self.audio_params, frames)
class DeviceNotFound(Exception):
"""Raises exception if audio capture device is not found."""