You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
518 lines
17 KiB
518 lines
17 KiB
/*
|
|
* Copyright (C) 2018 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.
|
|
*/
|
|
|
|
#define LOG_TAG "NBAIO_Tee"
|
|
//#define LOG_NDEBUG 0
|
|
|
|
#include <utils/Log.h>
|
|
|
|
#include <deque>
|
|
#include <dirent.h>
|
|
#include <future>
|
|
#include <list>
|
|
#include <vector>
|
|
|
|
#include <audio_utils/format.h>
|
|
#include <audio_utils/sndfile.h>
|
|
#include <media/nbaio/PipeReader.h>
|
|
|
|
#include "Configuration.h"
|
|
#include "NBAIO_Tee.h"
|
|
|
|
// Enabled with TEE_SINK in Configuration.h
|
|
#ifdef TEE_SINK
|
|
|
|
namespace android {
|
|
|
|
/*
|
|
Tee filenames generated as follows:
|
|
|
|
"aftee_Date_ThreadId_C_reason.wav" RecordThread
|
|
"aftee_Date_ThreadId_M_reason.wav" MixerThread (Normal)
|
|
"aftee_Date_ThreadId_F_reason.wav" MixerThread (Fast)
|
|
"aftee_Date_ThreadId_TrackId_R_reason.wav" RecordTrack
|
|
"aftee_Date_ThreadId_TrackId_TrackName_T_reason.wav" PlaybackTrack
|
|
|
|
where Date = YYYYmmdd_HHMMSS_MSEC
|
|
|
|
where Reason = [ DTOR | DUMP | REMOVE ]
|
|
|
|
Examples:
|
|
aftee_20180424_153811_038_13_57_2_T_REMOVE.wav
|
|
aftee_20180424_153811_218_13_57_2_T_REMOVE.wav
|
|
aftee_20180424_153811_378_13_57_2_T_REMOVE.wav
|
|
aftee_20180424_153825_147_62_C_DUMP.wav
|
|
aftee_20180424_153825_148_62_59_R_DUMP.wav
|
|
aftee_20180424_153825_149_13_F_DUMP.wav
|
|
aftee_20180424_153842_125_62_59_R_REMOVE.wav
|
|
aftee_20180424_153842_168_62_C_DTOR.wav
|
|
*/
|
|
|
|
static constexpr char DEFAULT_PREFIX[] = "aftee_";
|
|
static constexpr char DEFAULT_DIRECTORY[] = "/data/misc/audioserver";
|
|
static constexpr size_t DEFAULT_THREADPOOL_SIZE = 8;
|
|
|
|
/** AudioFileHandler manages temporary audio wav files with a least recently created
|
|
retention policy.
|
|
|
|
The temporary filenames are systematically generated. A common filename prefix,
|
|
storage directory, and concurrency pool are passed in on creating the object.
|
|
|
|
Temporary files are created by "create", which returns a filename generated by
|
|
|
|
prefix + 14 char date + suffix
|
|
|
|
TODO Move to audio_utils.
|
|
TODO Avoid pointing two AudioFileHandlers to the same directory and prefix
|
|
as we don't have a prefix specific lock file. */
|
|
|
|
class AudioFileHandler {
|
|
public:
|
|
|
|
AudioFileHandler(const std::string &prefix, const std::string &directory, size_t pool)
|
|
: mThreadPool(pool)
|
|
, mPrefix(prefix)
|
|
{
|
|
(void)setDirectory(directory);
|
|
}
|
|
|
|
/** returns filename of created audio file, else empty string on failure. */
|
|
std::string create(
|
|
std::function<ssize_t /* frames_read */
|
|
(void * /* buffer */, size_t /* size_in_frames */)> reader,
|
|
uint32_t sampleRate,
|
|
uint32_t channelCount,
|
|
audio_format_t format,
|
|
const std::string &suffix);
|
|
|
|
private:
|
|
/** sets the current directory. this is currently private to avoid confusion
|
|
when changing while pending operations are occurring (it's okay, but
|
|
weakly synchronized). */
|
|
status_t setDirectory(const std::string &directory);
|
|
|
|
/** cleans current directory and returns the directory name done. */
|
|
status_t clean(std::string *dir = nullptr);
|
|
|
|
/** creates an audio file from a reader functor passed in. */
|
|
status_t createInternal(
|
|
std::function<ssize_t /* frames_read */
|
|
(void * /* buffer */, size_t /* size_in_frames */)> reader,
|
|
uint32_t sampleRate,
|
|
uint32_t channelCount,
|
|
audio_format_t format,
|
|
const std::string &filename);
|
|
|
|
static bool isDirectoryValid(const std::string &directory) {
|
|
return directory.size() > 0 && directory[0] == '/';
|
|
}
|
|
|
|
std::string generateFilename(const std::string &suffix) const {
|
|
char fileTime[sizeof("YYYYmmdd_HHMMSS_\0")];
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
struct tm tm;
|
|
localtime_r(&tv.tv_sec, &tm);
|
|
LOG_ALWAYS_FATAL_IF(strftime(fileTime, sizeof(fileTime), "%Y%m%d_%H%M%S_", &tm) == 0,
|
|
"incorrect fileTime buffer");
|
|
char msec[4];
|
|
(void)snprintf(msec, sizeof(msec), "%03d", (int)(tv.tv_usec / 1000));
|
|
return mPrefix + fileTime + msec + suffix + ".wav";
|
|
}
|
|
|
|
bool isManagedFilename(const char *name) {
|
|
constexpr size_t FILENAME_LEN_DATE = 4 + 2 + 2 // %Y%m%d%
|
|
+ 1 + 2 + 2 + 2 // _H%M%S
|
|
+ 1 + 3; //_MSEC
|
|
const size_t prefixLen = mPrefix.size();
|
|
const size_t nameLen = strlen(name);
|
|
|
|
// reject on size, prefix, and .wav
|
|
if (nameLen < prefixLen + FILENAME_LEN_DATE + 4 /* .wav */
|
|
|| strncmp(name, mPrefix.c_str(), prefixLen) != 0
|
|
|| strcmp(name + nameLen - 4, ".wav") != 0) {
|
|
return false;
|
|
}
|
|
|
|
// validate date portion
|
|
const char *date = name + prefixLen;
|
|
return std::all_of(date, date + 8, isdigit)
|
|
&& date[8] == '_'
|
|
&& std::all_of(date + 9, date + 15, isdigit)
|
|
&& date[15] == '_'
|
|
&& std::all_of(date + 16, date + 19, isdigit);
|
|
}
|
|
|
|
// yet another ThreadPool implementation.
|
|
class ThreadPool {
|
|
public:
|
|
ThreadPool(size_t size)
|
|
: mThreadPoolSize(size)
|
|
{ }
|
|
|
|
/** launches task "name" with associated function "func".
|
|
if the threadpool is exhausted, it will launch on calling function */
|
|
status_t launch(const std::string &name, std::function<status_t()> func);
|
|
|
|
private:
|
|
std::mutex mLock;
|
|
std::list<std::pair<
|
|
std::string, std::future<status_t>>> mFutures; // GUARDED_BY(mLock)
|
|
|
|
const size_t mThreadPoolSize;
|
|
} mThreadPool;
|
|
|
|
const std::string mPrefix;
|
|
std::mutex mLock;
|
|
std::string mDirectory; // GUARDED_BY(mLock)
|
|
std::deque<std::string> mFiles; // GUARDED_BY(mLock) sorted list of files by creation time
|
|
|
|
static constexpr size_t FRAMES_PER_READ = 1024;
|
|
static constexpr size_t MAX_FILES_READ = 1024;
|
|
static constexpr size_t MAX_FILES_KEEP = 32;
|
|
};
|
|
|
|
/* static */
|
|
void NBAIO_Tee::NBAIO_TeeImpl::dumpTee(
|
|
int fd, const NBAIO_SinkSource &sinkSource, const std::string &suffix)
|
|
{
|
|
// Singleton. Constructed thread-safe on first call, never destroyed.
|
|
static AudioFileHandler audioFileHandler(
|
|
DEFAULT_PREFIX, DEFAULT_DIRECTORY, DEFAULT_THREADPOOL_SIZE);
|
|
|
|
auto &source = sinkSource.second;
|
|
if (source.get() == nullptr) {
|
|
return;
|
|
}
|
|
|
|
const NBAIO_Format format = source->format();
|
|
bool firstRead = true;
|
|
std::string filename = audioFileHandler.create(
|
|
// this functor must not hold references to stack
|
|
[firstRead, sinkSource] (void *buffer, size_t frames) mutable {
|
|
auto &source = sinkSource.second;
|
|
ssize_t actualRead = source->read(buffer, frames);
|
|
if (actualRead == (ssize_t)OVERRUN && firstRead) {
|
|
// recheck once
|
|
actualRead = source->read(buffer, frames);
|
|
}
|
|
firstRead = false;
|
|
return actualRead;
|
|
},
|
|
Format_sampleRate(format),
|
|
Format_channelCount(format),
|
|
format.mFormat,
|
|
suffix);
|
|
|
|
if (fd >= 0 && filename.size() > 0) {
|
|
dprintf(fd, "tee wrote to %s\n", filename.c_str());
|
|
}
|
|
}
|
|
|
|
/* static */
|
|
NBAIO_Tee::NBAIO_TeeImpl::NBAIO_SinkSource NBAIO_Tee::NBAIO_TeeImpl::makeSinkSource(
|
|
const NBAIO_Format &format, size_t frames, bool *enabled)
|
|
{
|
|
if (Format_isValid(format) && audio_is_linear_pcm(format.mFormat)) {
|
|
Pipe *pipe = new Pipe(frames, format);
|
|
size_t numCounterOffers = 0;
|
|
const NBAIO_Format offers[1] = {format};
|
|
ssize_t index = pipe->negotiate(offers, 1, NULL, numCounterOffers);
|
|
if (index != 0) {
|
|
ALOGW("pipe failure to negotiate: %zd", index);
|
|
goto exit;
|
|
}
|
|
PipeReader *pipeReader = new PipeReader(*pipe);
|
|
numCounterOffers = 0;
|
|
index = pipeReader->negotiate(offers, 1, NULL, numCounterOffers);
|
|
if (index != 0) {
|
|
ALOGW("pipeReader failure to negotiate: %zd", index);
|
|
goto exit;
|
|
}
|
|
if (enabled != nullptr) *enabled = true;
|
|
return {pipe, pipeReader};
|
|
}
|
|
exit:
|
|
if (enabled != nullptr) *enabled = false;
|
|
return {nullptr, nullptr};
|
|
}
|
|
|
|
std::string AudioFileHandler::create(
|
|
std::function<ssize_t /* frames_read */
|
|
(void * /* buffer */, size_t /* size_in_frames */)> reader,
|
|
uint32_t sampleRate,
|
|
uint32_t channelCount,
|
|
audio_format_t format,
|
|
const std::string &suffix)
|
|
{
|
|
const std::string filename = generateFilename(suffix);
|
|
|
|
if (mThreadPool.launch(std::string("create ") + filename,
|
|
[=]() { return createInternal(reader, sampleRate, channelCount, format, filename); })
|
|
== NO_ERROR) {
|
|
return filename;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
status_t AudioFileHandler::setDirectory(const std::string &directory)
|
|
{
|
|
if (!isDirectoryValid(directory)) return BAD_VALUE;
|
|
|
|
// TODO: consider using std::filesystem in C++17
|
|
DIR *dir = opendir(directory.c_str());
|
|
|
|
if (dir == nullptr) {
|
|
ALOGW("%s: cannot open directory %s", __func__, directory.c_str());
|
|
return BAD_VALUE;
|
|
}
|
|
|
|
size_t toRemove = 0;
|
|
decltype(mFiles) files;
|
|
|
|
while (files.size() < MAX_FILES_READ) {
|
|
errno = 0;
|
|
const struct dirent *result = readdir(dir);
|
|
if (result == nullptr) {
|
|
ALOGW_IF(errno != 0, "%s: readdir failure %s", __func__, strerror(errno));
|
|
break;
|
|
}
|
|
// is it a managed filename?
|
|
if (!isManagedFilename(result->d_name)) {
|
|
continue;
|
|
}
|
|
files.emplace_back(result->d_name);
|
|
}
|
|
(void)closedir(dir);
|
|
|
|
// OPTIMIZATION: we don't need to stat each file, the filenames names are
|
|
// already (roughly) ordered by creation date. we use std::deque instead
|
|
// of std::set for faster insertion and sorting times.
|
|
|
|
if (files.size() > MAX_FILES_KEEP) {
|
|
// removed files can use a partition (no need to do a full sort).
|
|
toRemove = files.size() - MAX_FILES_KEEP;
|
|
std::nth_element(files.begin(), files.begin() + toRemove - 1, files.end());
|
|
}
|
|
|
|
// kept files must be sorted.
|
|
std::sort(files.begin() + toRemove, files.end());
|
|
|
|
{
|
|
std::lock_guard<std::mutex> _l(mLock);
|
|
|
|
mDirectory = directory;
|
|
mFiles = std::move(files);
|
|
}
|
|
|
|
if (toRemove > 0) { // launch a clean in background.
|
|
(void)mThreadPool.launch(
|
|
std::string("cleaning ") + directory, [this]() { return clean(); });
|
|
}
|
|
return NO_ERROR;
|
|
}
|
|
|
|
status_t AudioFileHandler::clean(std::string *directory)
|
|
{
|
|
std::vector<std::string> filesToRemove;
|
|
std::string dir;
|
|
{
|
|
std::lock_guard<std::mutex> _l(mLock);
|
|
|
|
if (!isDirectoryValid(mDirectory)) return NO_INIT;
|
|
|
|
dir = mDirectory;
|
|
if (mFiles.size() > MAX_FILES_KEEP) {
|
|
size_t toRemove = mFiles.size() - MAX_FILES_KEEP;
|
|
|
|
// use move and erase to efficiently transfer std::string
|
|
std::move(mFiles.begin(),
|
|
mFiles.begin() + toRemove,
|
|
std::back_inserter(filesToRemove));
|
|
mFiles.erase(mFiles.begin(), mFiles.begin() + toRemove);
|
|
}
|
|
}
|
|
|
|
std::string dirp = dir + "/";
|
|
// remove files outside of lock for better concurrency.
|
|
for (const auto &file : filesToRemove) {
|
|
(void)unlink((dirp + file).c_str());
|
|
}
|
|
|
|
// return the directory if requested.
|
|
if (directory != nullptr) {
|
|
*directory = dir;
|
|
}
|
|
return NO_ERROR;
|
|
}
|
|
|
|
status_t AudioFileHandler::ThreadPool::launch(
|
|
const std::string &name, std::function<status_t()> func)
|
|
{
|
|
if (mThreadPoolSize > 1) {
|
|
std::lock_guard<std::mutex> _l(mLock);
|
|
if (mFutures.size() >= mThreadPoolSize) {
|
|
for (auto it = mFutures.begin(); it != mFutures.end();) {
|
|
const std::string &filename = it->first;
|
|
std::future<status_t> &future = it->second;
|
|
if (!future.valid() ||
|
|
future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
|
|
ALOGV("%s: future %s ready", __func__, filename.c_str());
|
|
it = mFutures.erase(it);
|
|
} else {
|
|
ALOGV("%s: future %s not ready", __func__, filename.c_str());
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
if (mFutures.size() < mThreadPoolSize) {
|
|
ALOGV("%s: deferred calling %s", __func__, name.c_str());
|
|
mFutures.emplace_back(name, std::async(std::launch::async, func));
|
|
return NO_ERROR;
|
|
}
|
|
}
|
|
ALOGV("%s: immediate calling %s", __func__, name.c_str());
|
|
return func();
|
|
}
|
|
|
|
status_t AudioFileHandler::createInternal(
|
|
std::function<ssize_t /* frames_read */
|
|
(void * /* buffer */, size_t /* size_in_frames */)> reader,
|
|
uint32_t sampleRate,
|
|
uint32_t channelCount,
|
|
audio_format_t format,
|
|
const std::string &filename)
|
|
{
|
|
// Attempt to choose the best matching file format.
|
|
// We can choose any sf_format
|
|
// but writeFormat must be one of 16, 32, float
|
|
// due to sf_writef compatibility.
|
|
int sf_format;
|
|
audio_format_t writeFormat;
|
|
switch (format) {
|
|
case AUDIO_FORMAT_PCM_8_BIT:
|
|
case AUDIO_FORMAT_PCM_16_BIT:
|
|
sf_format = SF_FORMAT_PCM_16;
|
|
writeFormat = AUDIO_FORMAT_PCM_16_BIT;
|
|
ALOGV("%s: %s using PCM_16 for format %#x", __func__, filename.c_str(), format);
|
|
break;
|
|
case AUDIO_FORMAT_PCM_8_24_BIT:
|
|
case AUDIO_FORMAT_PCM_24_BIT_PACKED:
|
|
case AUDIO_FORMAT_PCM_32_BIT:
|
|
sf_format = SF_FORMAT_PCM_32;
|
|
writeFormat = AUDIO_FORMAT_PCM_32_BIT;
|
|
ALOGV("%s: %s using PCM_32 for format %#x", __func__, filename.c_str(), format);
|
|
break;
|
|
case AUDIO_FORMAT_PCM_FLOAT:
|
|
sf_format = SF_FORMAT_FLOAT;
|
|
writeFormat = AUDIO_FORMAT_PCM_FLOAT;
|
|
ALOGV("%s: %s using PCM_FLOAT for format %#x", __func__, filename.c_str(), format);
|
|
break;
|
|
default:
|
|
// TODO:
|
|
// handle audio_has_proportional_frames() formats.
|
|
// handle compressed formats as single byte files.
|
|
return BAD_VALUE;
|
|
}
|
|
|
|
std::string directory;
|
|
status_t status = clean(&directory);
|
|
if (status != NO_ERROR) return status;
|
|
std::string dirPrefix = directory + "/";
|
|
|
|
const std::string path = dirPrefix + filename;
|
|
|
|
/* const */ SF_INFO info = {
|
|
.frames = 0,
|
|
.samplerate = (int)sampleRate,
|
|
.channels = (int)channelCount,
|
|
.format = SF_FORMAT_WAV | sf_format,
|
|
};
|
|
SNDFILE *sf = sf_open(path.c_str(), SFM_WRITE, &info);
|
|
if (sf == nullptr) {
|
|
return INVALID_OPERATION;
|
|
}
|
|
|
|
size_t total = 0;
|
|
void *buffer = malloc(FRAMES_PER_READ * std::max(
|
|
channelCount * audio_bytes_per_sample(writeFormat), //output framesize
|
|
channelCount * audio_bytes_per_sample(format))); // input framesize
|
|
if (buffer == nullptr) {
|
|
sf_close(sf);
|
|
return NO_MEMORY;
|
|
}
|
|
|
|
for (;;) {
|
|
const ssize_t actualRead = reader(buffer, FRAMES_PER_READ);
|
|
if (actualRead <= 0) {
|
|
break;
|
|
}
|
|
|
|
// Convert input format to writeFormat as needed.
|
|
if (format != writeFormat) {
|
|
memcpy_by_audio_format(
|
|
buffer, writeFormat, buffer, format, actualRead * info.channels);
|
|
}
|
|
|
|
ssize_t reallyWritten;
|
|
switch (writeFormat) {
|
|
case AUDIO_FORMAT_PCM_16_BIT:
|
|
reallyWritten = sf_writef_short(sf, (const int16_t *)buffer, actualRead);
|
|
break;
|
|
case AUDIO_FORMAT_PCM_32_BIT:
|
|
reallyWritten = sf_writef_int(sf, (const int32_t *)buffer, actualRead);
|
|
break;
|
|
case AUDIO_FORMAT_PCM_FLOAT:
|
|
reallyWritten = sf_writef_float(sf, (const float *)buffer, actualRead);
|
|
break;
|
|
default:
|
|
LOG_ALWAYS_FATAL("%s: %s writeFormat: %#x", __func__, filename.c_str(), writeFormat);
|
|
break;
|
|
}
|
|
|
|
if (reallyWritten < 0) {
|
|
ALOGW("%s: %s write error: %zd", __func__, filename.c_str(), reallyWritten);
|
|
break;
|
|
}
|
|
total += reallyWritten;
|
|
if (reallyWritten < actualRead) {
|
|
ALOGW("%s: %s write short count: %zd < %zd",
|
|
__func__, filename.c_str(), reallyWritten, actualRead);
|
|
break;
|
|
}
|
|
}
|
|
sf_close(sf);
|
|
free(buffer);
|
|
if (total == 0) {
|
|
(void)unlink(path.c_str());
|
|
return NOT_ENOUGH_DATA;
|
|
}
|
|
|
|
// Success: add our name to managed files.
|
|
{
|
|
std::lock_guard<std::mutex> _l(mLock);
|
|
// weak synchronization - only update mFiles if the directory hasn't changed.
|
|
if (mDirectory == directory) {
|
|
mFiles.emplace_back(filename); // add to the end to preserve sort.
|
|
}
|
|
}
|
|
return NO_ERROR; // return full path
|
|
}
|
|
|
|
} // namespace android
|
|
|
|
#endif // TEE_SINK
|