/* * 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