背景:
在做audio相关的开发时候,如果遇到某个声音不正常现象,这个时候就可能需要定位具体哪里出了问题。整个音频流程中,首先是各个app的AudioTrack的数据,传入AudioFlinger,由AudioFlinger进行了混音后,再送入Hal,kernel等。

那么到底声音是哪里出了问题,是混音前,还是混音后?有什么好的方式确定么?
这块其实马哥前面就有分享过一篇关于android官方调试方案Tee sink使用dump上面两个过程的音频数据文章:
https://blog.youkuaiyun.com/learnframework/article/details/140035473
https://mp.weixin.qq.com/s/EAjlen6S05vawb0U3bblCQ
但当时也只是教大家如何使用,并没有深入剖析Tee sink具体代码是如何实现,它是怎么做到的可以dump出上面过程的数据。
那么本文马哥就带大家来深入剖析一下Tee sink的源码分析,因为理解了源码后,后续大家如果想要在其他地方或者场景要额外dump出数据,或者这种Tee sink的调试方案也有了很好的参考。
Tee sink的使用方案:
大家可以直接参考官方地址:
https://source.android.google.cn/docs/core/audio/debugging
Tee Sink
“tee sink”是一种 AudioFlinger 调试功能,仅在定制 build 中提供,用于获取最近音频的短片段以供日后分析。 这方便我们比较实际播放或录制的内容与预期内容。
出于隐私考虑,tee sink 在编译时和运行时均默认处于停用状态。如需使用 tee sink,您需要通过重新编译以及设置属性来启用它。完成调试后,请务必停用此功能;tee sink 在正式 build 中不能处于启用状态。
本部分中的说明适用于 Android 7.x 及更高版本。对于 Android 5.x 和 6.x,请将 /data/misc/audioserver 替换为 /data/misc/media。此外,您还必须使用 userdebug 或 eng build。如果您使用 userdebug 版本,请使用以下命令停用 verity:
adb root && adb disable-verity && adb reboot
编译时设置
cd frameworks/av/services/audioflinger
修改 Configuration.h。
对 #define TEE_SINK 取消注释。
重新构建 libaudioflinger.so。
adb root
adb remount
将新的 libaudioflinger.so 推送或同步到设备的 /system/lib。
运行时设置
adb shell getprop | grep ro.debuggable
确认输出是:[ro.debuggable]: [1]
adb shell
ls -ld /data/misc/audioserver
确认输出是:
drwx------ media media ... media
如果目录不存在,请按如下方式创建:
mkdir /data/misc/audioserver
chown media:media /data/misc/audioserver
echo af.tee=# > /data/local.prop
其中,af.tee 值是一个数字,在下文中有所说明。
chmod 644 /data/local.prop
reboot
af.tee 属性的值
af.tee 的值是一个 0 到 7 之间的数字,表示几个位的总和(每个功能一个位)。请查看位于 AudioFlinger.cpp 中的 AudioFlinger::AudioFlinger() 的代码,了解各个位的说明,简单来说就是:
1 = 输入
2 = FastMixer 输出
4 = 各音轨的 AudioRecord 和 AudioTrack
目前还没有针对深度缓冲区和常规混合器的位,不过您可以使用“4”获取类似结果。
测试和获取数据
运行您的音频测试。
adb shell dumpsys media.audio_flinger
在 dumpsys 输出中查找如下行:
tee copied to /data/misc/audioserver/20131010101147_2.wav
这是一个 PCM .wav 文件。
然后,使用 adb pull 命令提取任何相关的 /data/misc/audioserver/*.wav 文件;请注意,音轨特定的转储文件名不会显示在 dumpsys 输出中,但仍会在音轨关闭时保存到 /data/misc/audioserver。
在与其他人分享之前,请先查看转储文件是否涉及隐私权问题。
建议
要获取更实用的结果,请尝试以下方法:
停用触摸提示音和按键音,以减少测试输出过程中的中断。
将所有音量调到最大。
停用通过麦克风发出声音或录音的应用(如果这些应用与测试无关)。
音轨特定的转储文件仅在音轨关闭时保存;您可能需要强制关闭某个应用才能转储其音轨特定的数据。
在测试后立即执行 dumpsys;可用的录制空间是有限的。
如需确保转储文件不会丢失,请定期将其上传到您的主机。 您只能保留有限数量的转储文件;达到此数量上限后,系统会移除较旧的转储文件。
恢复
如上文所述,tee sink 功能不能保持启用状态。 如需恢复您的 build 和设备,请执行以下操作:
还原对 Configuration.h 所做的源代码更改。
重新构建 libaudioflinger.so。
将恢复后的 libaudioflinger.so 推送或同步到设备的 /system/lib。
adb shell
rm /data/local.prop
rm /data/misc/audioserver/*.wav
reboot
上面文档已经很详细介绍了具体使用,这里再总结一下Tee sink使用注意点:
1、Tee sink只能在debug或者eng版本使用
2、Tee sink需要重新修改编译代码替换so才可以使用
3、需要通过/data/local.prop中的af.tee=#的值来控制具体导出哪些数据,大家图方便也可以直接传递为af.tee=7,这样数据就会比较多
4、dumpsys media.audio_flinger命令主动导出数据到wav,但是像track数据这种不需要主动dump就会自动有,因为track播放完成就会自己remove
Tee sink源码剖析
源码位置
frameworks/av/services/audioflinger/Configuration.h
----控制Tee sink功能是否开放
frameworks/av/services/audioflinger/afutils/NBAIO_Tee.cpp
frameworks/av/services/audioflinger/afutils/NBAIO_Tee.h
—控制Tee sink核心实现部分,会提供相关接口外部调用写入Tee sink数据
frameworks/av/services/audioflinger/Threads.cpp
frameworks/av/services/audioflinger/Tracks.cpp
各个数据源头业务代码,会主动调用Tee sink相关的接口进行数据写入。
好了,大概知道Tee sink涉及的代码后,下面就开始分析源码了。
开放Tee sink功能部分
默认情况下Tee sink功能是关闭的,需要主动去Configuration.h放开
frameworks/av/services/audioflinger/Configuration.h
可以看到默认情况下是没有定义TEE_SINK这个宏,需要放开注释,放开注释以后,在Threads.cpp等地方才可以编译对应的Tee sink相关业务。


Threads,Tracks使用Tee sink部分
Tee sink本质就是导出相关的track和mixerthread的数据,这些数据具体导出后是怎么样的呢?
先来看看这里导出偶的音频:
对于声音输出上面主要有2类:
track类型数据
aftee_20250904_102305_917_13_55_T_REMOVE.wav
这种带有_REMOVE主要代表的是track的数据,因为track一般使用完了就会进行remove,所以一般track的数据是不需要进行主动的dump才生成的,是每次track 在remove时候就会有。
这里名字解释一下:
aftee_20250904_102305_917_13_55_T_REMOVE
这里需要进行拆分一下这个名字。
第1部分 “aftee_”字符 代表tee sink的前缀
第2部分 “20250904_102305_917”代表时间
第3部分 “13_55_T”代表Track所在Thread的的IoHandle值和trackId,13就是Output Thread的Id,55代表这个Track的id
第4部分 “_REMOVE”代表在Track移除时候进行导出
MixerThread类型数据
aftee_20250904_102326_803_13_M_DUMP.wav
这种带有M_DUMP数据就是MixerThread的混音数据,它的前面2部分都和上面track一样,只有第三方部分有差异。
第3部分 “13_M”代表MixerThread的Id,“M”就代表MixerThread
第4部分 “DUMP”代表是dump时候触发的
上面进行文件名字部分的源码分析:
track部分:
frameworks/av/services/audioflinger/afutils/NBAIO_Tee.cpp
std::string generateFilename(const std::string &suffix, audio_format_t format) const {
char fileTime[sizeof("YYYYmmdd_HHMMSS_\0")];
struct timeval tv;
gettimeofday(&tv, nullptr /* struct timezone */);
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));
//下面就是字符拼接最后的名字,可以看到这里最核心的就是suffix
return mPrefix + fileTime + msec + suffix + (audio_is_linear_pcm(format) ? ".wav" : ".raw");
}
下面来探索最难的suffix这部分的字符。
generateFilename这个方法是如下AudioFileHandler::create调用的。
frameworks/av/services/audioflinger/afutils/NBAIO_Tee.cpp
std::string AudioFileHandler::create(
const 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)
{
std::string filename = generateFilename(suffix, format);
//省略
return "";
}
那么AudioFileHandler::create又是哪里调用的呢?
这里查到了发现是NBAIO_TeeImpl::dumpTee方法传递进来的
/* 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);
const 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);
}
那么NBAIO_TeeImpl::dumpTee又是谁调用的呢?
这里可能稍微有点难找到,最后发现是在头文件中进行的调用
frameworks/av/services/audioflinger/afutils/NBAIO_Tee.h
class NBAIO_TeeImpl {
public:
//对mId进行设置
void setId(const std::string &id) {
const std::lock_guard<std::mutex> _l(mLock);
//这里会对mId进行赋值
mId = id;
}
//可以看到NBAIO_TeeImpl的dump方法会触发的dumpTee,这里dump只传递了一个字符reason
void dump(int fd, const std::string &reason) {
if (!mDataReady.exchange(false)) return;
std::string suffix;
NBAIO_SinkSource sinkSource;
{
const std::lock_guard<std::mutex> _l(mLock);
//注意这类的suffix实际上是mId和reason一起拼凑的
suffix = mId + reason;
sinkSource = mSinkSource;
}
dumpTee(fd, sinkSource, suffix);
}
上面可以看出的suffix本质是mId + reason,reason好理解属于dump方法传递进来的,核心就是mId是怎么来的?其实上面代码也看得出是使用setId方法来进行的id设置。
那么这里的setId和dump方法是哪里调用的呢?
setId方法调用:
frameworks/av/services/audioflinger/Tracks.cpp
Track::Track(
IAfPlaybackThread* thread,
const sp<Client>& client,
audio_stream_type_t streamType,
const audio_attributes_t& attr,
uint32_t sampleRate,
audio_format_t format,
audio_channel_mask_t channelMask,
size_t frameCount,
void *buffer,
size_t bufferSize,
const sp<IMemory>& sharedBuffer,
audio_session_t sessionId,
pid_t creatorPid,
const AttributionSourceState& attributionSource,
audio_output_flags_t flags,
track_type type,
audio_port_handle_t portId,
size_t frameCountToBeReady,
float speed,
bool isSpatialized,
bool isBitPerfect)
{
//省略部分
#ifdef TEE_SINK
//在Track的构造时候,进行了setId,包含mThreadIoHandle+mId+“T”
mTee.setId(std::string("_") + std::to_string(mThreadIoHandle)
+ "_" + std::to_string(mId) + "_T");
#endif
可以看出,在Track的构造时候,进行了setId,包含mThreadIoHandle+mId+“T”。
dump方法的调用:
frameworks/av/services/audioflinger/Threads.cpp
template <typename T>
ssize_t ThreadBase::ActiveTracks<T>::remove(const sp<T>& track) {
#ifdef TEE_SINK
//注意这里调用了track的dumpTee,传递了"_REMOVE"字符
track->dumpTee(-1 /* fd */, "_REMOVE");
#endif
return index;
}
在ActiveTracks进行remove时候才会进行调用 track->dumpTee,再看看这里的dumpTee方法:
frameworks/av/services/audioflinger/TrackBase.h
#ifdef TEE_SINK
void dumpTee(int fd, const std::string &reason) const final {
//这里就真正调用到了mTee.dump方法
mTee.dump(fd, reason);
}
#endif
直接最后上一个堆栈来调用流程

到这里就搞清楚了track的文件名字来源,那么它的数据是来自哪里?又是如何写入文件中?
数据的来源其实在Track进行releaseBuffer时候写入的
// AudioBufferProvider interface
// getNextBuffer() = 0;
// This implementation of releaseBuffer() is used by Track and RecordTrack
void TrackBase::releaseBuffer(AudioBufferProvider::Buffer* buffer)
{
#ifdef TEE_SINK
mTee.write(buffer->raw, buffer->frameCount);
#endif
ServerProxy::Buffer buf;
buf.mFrameCount = buffer->frameCount;
buf.mRaw = buffer->raw;
buffer->frameCount = 0;
buffer->raw = NULL;
mServerProxy->releaseBuffer(&buf);
}
明显看到这里调用了mTee.write方法把数据写入Tee sink,具体实现如下:
frameworks/av/services/audioflinger/afutils/NBAIO_Tee.h
/** The underlying implementation of the Tee - the lifetime is through
a shared pointer so destruction of the NBAIO_Tee container may proceed
even though dumping is occurring. */
class NBAIO_TeeImpl {
public:
void write(const void *buffer, size_t frameCount) {
//注意这里会进行mEnabled的判断如果为false不会进行写入
if (!mEnabled.load() || frameCount == 0) return;
//这里会调用mSinkSource进行buffer写入
(void)mSinkSource.first->write(buffer, frameCount);
mDataReady.store(true);
}
可以看出这里有一个很关键mEnabled和mSinkSource值,那么这个值是哪里设置的呢?
mEnabled值的设置部分
其实这里的mEnabled是在set方法中进行的设置:
status_t set(const NBAIO_Format &format, TEE_FLAG flags, size_t frames) {
//这里会有核心方法对我们设置prop中的af.tee值获取
static const int teeConfig = property_get_bool("ro.debuggable", false)
? property_get_int32("af.tee", 0) : 0;
// check the type of Tee
const TEE_FLAG type = TEE_FLAG(
flags & (TEE_FLAG_INPUT_THREAD | TEE_FLAG_OUTPUT_THREAD | TEE_FLAG_TRACK));
//这里会对af.tee值与(TEE_FLAG_INPUT_THREAD | TEE_FLAG_OUTPUT_THREAD | TEE_FLAG_TRACK)进行比较,如果都没有开放,那就是返回权限不足
// if type is set, we check to see if it is permitted by configuration.
if (type != 0 && (type & teeConfig) == 0) {
return PERMISSION_DENIED;
}
//省略
bool enabled = false;
//走到这里会进行PipeReader管道创建,一般都可以成功创建
auto sinksource = makeSinkSource(format, frames, &enabled);
// enabled is set if makeSinkSource is successful.
// Note: as mentioned in NBAIO_Tee::set(), don't call set() while write() is
// ongoing.
if (enabled) {
const std::lock_guard<std::mutex> _l(mLock);
mFlags = flags;
mFormat = format; // could get this from the Sink.
mFrames = frames;
mSinkSource = std::move(sinksource);//注意给mSinkSource进行了赋值
//mEnabled的值被保存为true
mEnabled.store(true);
return NO_ERROR;
}
return BAD_VALUE;
}
上面方法比较核心:
1、判断我们前面prop中的设置的af.tee是符合以下几个值:
enum TEE_FLAG {
TEE_FLAG_NONE = 0,
TEE_FLAG_INPUT_THREAD = (1 << 0), // treat as a Tee for input (Capture) Threads
TEE_FLAG_OUTPUT_THREAD = (1 << 1), // treat as a Tee for output (Playback) Threads
TEE_FLAG_TRACK = (1 << 2), // treat as a Tee for tracks (Record and Playback)
};
即输入,输出,Track
一旦不满足,那么就直接返回,不会吧mEnabled进行设置。
2、还需要创建相关的PipeReader,赋值给mSinkSource
那么上面set方法哪里被调用呢?其实是在TrackBase的构造方法中进行set调用:
TrackBase::TrackBase(
IAfThreadBase *thread,
const sp<Client>& client,
const audio_attributes_t& attr,
uint32_t sampleRate,
audio_format_t format,
audio_channel_mask_t channelMask,
size_t frameCount,
void *buffer,
size_t bufferSize,
audio_session_t sessionId,
pid_t creatorPid,
uid_t clientUid,
bool isOut,
const alloc_type alloc,
track_type type,
audio_port_handle_t portId,
std::string metricsId)
{
#ifdef TEE_SINK
mTee.set(sampleRate, mChannelCount, format, NBAIO_Tee::TEE_FLAG_TRACK);
#endif
}
}
数据在哪写入的文件?
这里就需要回到最看是的dumpTee方法了
void dump(int fd, const std::string &reason) {
if (!mDataReady.exchange(false)) return;
std::string suffix;
NBAIO_SinkSource sinkSource;
{
const std::lock_guard<std::mutex> _l(mLock);
suffix = mId + reason;
sinkSource = mSinkSource;//把上面的mSinkSource赋值给sinkSource
}
//调用dumpTee又传递了sinkSource
dumpTee(fd, sinkSource, suffix);
}
再看看dumpTee方法
void NBAIO_Tee::NBAIO_TeeImpl::dumpTee(
int fd, const NBAIO_SinkSource &sinkSource, const std::string &suffix)
{
//省略
const 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);
}
可以看到这里的audioFileHandler.create有传递一个lamada表达式
std::string AudioFileHandler::create(
const 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)
{
std::string filename = generateFilename(suffix, format);
//这里会使用线程池调用createInternal方法
if (mThreadPool.launch(std::string("create ") + filename,
[=]() { return createInternal(reader, sampleRate, channelCount, format, filename); })
== NO_ERROR) {
return filename;
}
return "";
}
下面看看真正的createInternal方法
status_t AudioFileHandler::createInternal(
const 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)
{
//省略
//根据文件名字打开文件进行写入数据
SNDFILE *sf = sf_open(path.c_str(), SFM_WRITE, &info);
//省略
for (;;) {
const ssize_t actualRead = reader(buffer, FRAMES_PER_READ);
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;
break;
}
}
//省略
return NO_ERROR; // return full path
}
MixerThread部分:
上面track部分其实已经分析差不多了,唯一不同在于MixerThread的数据写入和dump时机不一样,这里重点说这部分差异:
// shared by MIXER and DIRECT, overridden by DUPLICATING
ssize_t PlaybackThread::threadLoop_write()
{
// If an NBAIO sink is present, use it to write the normal mixer's submix
if (mNormalSink != 0) {
ssize_t framesWritten = mNormalSink->write((char *)mSinkBuffer + offset, count);
if (framesWritten > 0) {
bytesWritten = framesWritten * mFrameSize;
#ifdef TEE_SINK
//进tee sink的write调用
mTee.write((char *)mSinkBuffer + offset, framesWritten);
#endif
return bytesWritten;
}
可以看到MixerThread数据是在PlaybackThread::threadLoop_write方法中进行的写入。
再看看dump时机:
status_t AudioFlinger::dump(int fd, const Vector<String16>& args)
NO_THREAD_SAFETY_ANALYSIS // conditional try lock
{
//省略
#ifdef TEE_SINK
// NBAIO_Tee dump is safe to call outside of AF lock.
NBAIO_Tee::dumpAll(fd, "_DUMP");
#endif
frameworks/av/services/audioflinger/afutils/NBAIO_Tee.h
/**
* \brief dump all Tees currently alive.
*
* \param fd file descriptor to write dumped filename for logging, use -1 to ignore.
* \param reason string suffix to append to the generated file.
*/
static void dumpAll(int fd, const std::string &reason = "") {
getRunningTees().dump(fd, reason);
}
那么getRunningTees呢?
/* static */
NBAIO_Tee::RunningTees& NBAIO_Tee::getRunningTees() {
[[clang::no_destroy]] static RunningTees runningTees;
return runningTees;
}
那么这里的RunningTees集合中加入:
NBAIO_Tee()
: mTee(std::make_shared<NBAIO_TeeImpl>())
{
getRunningTees().add(mTee);
}
可以看出这里是NBAIO_Tee()构造时候加入,构造这里又是每个ThreadBase就会自带构造NBAIO_Tee
frameworks/av/services/audioflinger/Threads.h
#ifdef TEE_SINK
NBAIO_Tee mTee;
#endif
这里的看看调用堆栈

文章来源:
https://mp.weixin.qq.com/s/EAjlen6S05vawb0U3bblCQ
更多audio框架相关实战干货,请关注下面“千里马学框架”
2185

被折叠的 条评论
为什么被折叠?



