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