深入Audio debugging调试之Tee sink源码剖析

背景:

在做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 的值是一个 07 之间的数字,表示几个位的总和(每个功能一个位)。请查看位于 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框架相关实战干货,请关注下面“千里马学框架”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里马学框架

帮助你了,就请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值