高通audio offload学习

简述

offload在音频系统里面,就是将对于音频文件的解码操作过载到DSP中去做,比如说mp3的解码操作不是在mediaserver中来做,而是直接将数据传递到dsp中,由DSP去解码和播放,就是所谓的硬解。硬解的优势是省功耗,除了DSP自身在处理音频格式的时候功耗相对较低,整个数据在安卓的音频系统中传递也会相对低些,前面那个好理解,后面这个我们结合代码来分析。

正文

这次的场景,就是在高通平台通过mediaplayer进行播放mp3.其中audiotrack中采用到了callback的方式。

audiotrack部分

我们从代码的角度一步步分析。
摘取一些audiotrack中set中的代码:

if (cbf != NULL) {
    mAudioTrackThread = new AudioTrackThread(*this, threadCanCallJava);
    mAudioTrackThread->run("AudioTrack", ANDROID_PRIORITY_AUDIO, 0 /*stack*/);
    // thread begins in paused state, and will not reference us until start()
}

这部分创建了AudioTrackThread,并且让它跑起来了。来看下线程的逻辑处理函数:

bool AudioTrack::AudioTrackThread::threadLoop()
{
    {
        AutoMutex _l(mMyLock);
        // mPaused 外部触发的pause,将会在这边进行block
        if (mPaused) {
            mMyCond.wait(mMyLock);
            // caller will check for exitPending()
            return true;
        }
        // 如果忽略下一次的内部pause, 下面的内部pause将会被跳过
        if (mIgnoreNextPausedInt) {
            mIgnoreNextPausedInt = false;
            mPausedInt = false;
        }
        // 内部pause,如果有内部pause的需求,将在这边进行等待。
        if (mPausedInt) {
            if (mPausedNs > 0) {
                (void) mMyCond.waitRelative(mMyLock, mPausedNs);
            } else {
                mMyCond.wait(mMyLock);
            }
            mPausedInt = false;
            return true;
        }
    }
    // 如果线程结束,则返回false,停止循环
    if (exitPending()) {
        return false;
    }
    // 这边将进行获取数据,处理数据的操作,返回的ns决定将内部block多久,跟开头的那边对应
    nsecs_t ns = mReceiver.processAudioBuffer();
    switch (ns) {
    case 0:
        return true;
    case NS_INACTIVE:
        pauseInternal();
        return true;
    case NS_NEVER:
        return false;
    case NS_WHENEVER:
        // Event driven: call wake() when callback notifications conditions change.
        ns = INT64_MAX;
        // fall through
    default:
        LOG_ALWAYS_FATAL_IF(ns < 0, "processAudioBuffer() returned %" PRId64, ns);
        pauseInternal(ns);
        return true;
    }
}

上面函数大部分都在描述如何控制暂停,对于数据的处理在函数processAudioBuffer中,我们可以回头来看下这个函数的实现:

nsecs_t AudioTrack::processAudioBuffer()
{

    mLock.lock();
    // 申请线程成实时线程,这边进行循环检测,是否申请成功,每次休眠的时间成指数增长,最多循环5次
    if (mAwaitBoost) {
        mAwaitBoost = false;
        mLock.unlock();
        static const int32_t kMaxTries = 5;
        int32_t tryCounter = kMaxTries;
        uint32_t pollUs = 10000;
        do {
            int policy = sched_getscheduler(0);
            if (policy == SCHED_FIFO || policy == SCHED_RR) {
                break;
            }
            usleep(pollUs);
            pollUs <<= 1;
        } while (tryCounter-- > 0);
        if (tryCounter < 0) {
            ALOGE("did not receive expected priority boost on time");
        }
        // Run again immediately
        return 0;
    }

    // Can only reference mCblk while locked
    int32_t flags = android_atomic_and(
        ~(CBLK_UNDERRUN | CBLK_LOOP_CYCLE | CBLK_LOOP_FINAL | CBLK_BUFFER_END), &mCblk->mFlags);

    // Check for track invalidation
    if (flags & CBLK_INVALID) {
        // 对于offload tracks的restoreTrack_l将只会更新sequence和清空AudioSystem缓存。
        // 我们不会在这边直接退出,但是后面会调用callback来让上层recreate该track
        // 对于其他类型的track,将在这边进行restore的操作,并且不会让他在这边退出,因为flag中还要处理
        if (!isOffloadedOrDirect_l() || (mSequence == mObservedSequence)) {
            status_t status __unused = restoreTrack_l("processAudioBuffer");
        }
    }
    // 如果当前正在stopping状态的话,则设置waitStreamEnd为true
    bool waitStreamEnd = mState == STATE_STOPPING;
    // 判断当前该track的状态是否正在播放
    bool active = mState == STATE_ACTIVE;

    // Manage underrun callback, must be done under lock to avoid race with releaseBuffer()
    bool newUnderrun = false;
    if (flags & CBLK_UNDERRUN) {
        if (!mInUnderrun) {
            mInUnderrun = true;
            newUnderrun = true;
        }
    }

    // Get current position of server 更新server的position
    size_t position = updateAndGetPosition_l();

    // Cache other fields that will be needed soon
    uint32_t sampleRate = mSampleRate;
    float speed = mPlaybackRate.mSpeed;
    const uint32_t notificationFrames = mNotificationFramesAct;
    // 如果需要更新remaing则将notificationFrames更新到mRemainingFrames,一般在开始或者flush之后。
    if (mRefreshRemaining) {
        mRefreshRemaining = false;
        mRemainingFrames = notificationFrames;
        mRetryOnPartialBuffer = false;
    }
    size_t misalignment = mProxy->getMisalignment();
    uint32_t sequence = mSequence;
    sp<AudioTrackClientProxy> proxy = mProxy;

    mLock.unlock();

    // get anchor time to account for callbacks.
    const nsecs_t timeBeforeCallbacks = systemTime();
    // 处理留马上结束的逻辑
    if (waitStreamEnd) {
        struct timespec timeout;
        // 120s
        timeout.tv_sec = WAIT_STREAM_END_TIMEOUT_SEC;
        timeout.tv_nsec = 0;
        // 这边正常情况下会进入休眠,等待最长时间为2分钟
        status_t status = proxy->waitStreamEndDone(&timeout);
        switch (status) {
        case NO_ERROR:
        case DEAD_OBJECT:
        case TIMED_OUT:
            // 告诉上层stream end的事件
            mCbf(EVENT_STREAM_END, mUserData, NULL);
            {
                AutoMutex lock(mLock);
                // 更新确认下是否还处在waitStreamEnd状态。如果是的话,切换state到stoped状态
                waitStreamEnd = mState == STATE_STOPPING;
                if (waitStreamEnd) {
                    mState = STATE_STOPPED;
                    mReleased = 0;
                }
            }
            // 如果这个track没有出现什么异常,并且已经stream end了,就进入休眠了
            if (waitStreamEnd && status != DEAD_OBJECT) {
               return NS_INACTIVE;
            }
            // 如果不是,则直接跳出返回0,这样该函数再被调用一次。
            break;
        }
        return 0;
    }

    if (flags & CBLK_BUFFER_END) {
        mCbf(EVENT_BUFFER_END, mUserData, NULL);
    }

    // 如果当前处在非激活状态,则进入休眠等待到该track被重启
    if (!active) {
        return NS_INACTIVE;
    }


    // If > 0, poll periodically to recover from a stuck server.  A good value is 2.
    static const uint32_t kPoll = 0;
    if (kPoll > 0 && mTransfer == TRANSFER_CALLBACK && kPoll * notificationFrames < minFrames) {
        minFrames = kPoll * notificationFrames;
    }

    // This "fudge factor" avoids soaking CPU, and compensates for late progress by server
    static const nsecs_t kWaitPeriodNs = WAIT_PERIOD_MS * 1000000LL;
    const nsecs_t timeAfterCallbacks = systemTime();

    // Convert frame units to time units
    nsecs_t ns = NS_WHENEVER;

    // If not supplying data by EVENT_MORE_DATA, then we're done
    if (mTransfer != TRANSFER_CALLBACK) {
        return ns;
    }
    // 阻塞时间的换算
    struct timespec timeout;
    const struct timespec *requested = &ClientProxy::kForever;
    if (ns != NS_WHENEVER) {
        timeout.tv_sec = ns / 1000000000LL;
        timeout.tv_nsec = ns % 1000000000LL;
        requested = &timeout;
    }
    // 开始进入获取buffer,填充buffer,release buffer的逻辑
    while (mRemainingFrames > 0) {

        Buffer audioBuffer;
        audioBuffer.frameCount = mRemainingFrames;
        size_t nonContig;
        // 第一次进入该循环,obtain是个阻塞操作,后面就变成非阻塞操作。这个主要跟buffer的设计思路有关。
        // avail buffer有可能包含两部分,一部分在前面,一部分在后面。而一开始我们只能获取后面部分的。
        // 在循环一次将触发获取前面部分的数据。
        status_t err = obtainBuffer(&audioBuffer, requested, NULL, &nonContig);
        // 修改获取策略为nonBlocking
        requested = &ClientProxy::kNonBlocking;
        // 计算上次获取到的总的空间大小,此次只能填充audioBuffer.frameCount
        size_t avail = audioBuffer.frameCount + nonContig;

        size_t reqSize = audioBuffer.size;
        // 问app要数据
        mCbf(EVENT_MORE_DATA, mUserData, &audioBuffer);
        size_t writtenSize = audioBuffer.size;
        // 如果写入的数据为0,则返回给休眠的时间,省得多做无用功
        if (writtenSize == 0) {
            nsecs_t myns;
            if (audio_is_linear_pcm(mFormat)) {
            } else {
                myns = kWaitPeriodNs;
            }
            if (ns > 0) { // account for obtain and callback time
                const nsecs_t timeNow = systemTime();
                ns = max((nsecs_t)0, ns - (timeNow - timeAfterCallbacks));
            }
            if (ns < 0 /* NS_WHENEVER */ || myns < ns) {
                ns = myns;
            }
            return ns;
        }

        size_t releasedFrames = writtenSize / mFrameSize;
        audioBuffer.frameCount = releasedFrames;
        mRemainingFrames -= releasedFrames;
        if (misalignment >= releasedFrames) {
            misalignment -= releasedFrames;
        } else {
            misalignment = 0;
        }
        // 释放缓冲区,其实就是更新了下cblk的里面的参数
        releaseBuffer(&audioBuffer);
        // 写的数据小宇reqSize则循环再获取,填充等操作。
        if (writtenSize < reqSize) {
            continue;
        }
        // 如果buffer的前部分大于等于剩余的需要获取的数据量,则继续问app要来填充buffer
        if (mRemainingFrames <= nonContig) {
            continue;
        }
    }
    // 更新mRemainingFrames为整个buffer大小。notificationFrames这个值在offload的场景下为buffer大小
    mRemainingFrames = notificationFrames;
    mRetryOnPartialBuffer = true;
    return 0;
}

上面基本上就完成了怎么问上层要数据的逻辑了。下面代码说明mNotificationFramesAct值来自何处

下面摘取createTrack_l中的部分代码:

// mNotificationFramesAct 等于buffer的大小
if (!audio_is_linear_pcm(mFormat)) {

    if (mSharedBuffer != 0) {
        // Same comment as below about ignoring frameCount parameter for set()
        frameCount = mSharedBuffer->size();
    } else if (frameCount == 0) {
        frameCount = mAfFrameCount;
    }
    if (mNotificationFramesAct != frameCount) {
        mNotificationFramesAct = frameCount;
    }
} ...

mNotificationFramesAct这个值我们之前有谈过了,这个值是用来确定该什么时候唤醒生产者往buffer里面填充数据,但是这个值如果大于mFrameCount的一半的话,那通知的大小将会采用mFrameCount的一半。

到这边,我们就先完成offload的audiotrack部分通过线程获取数据的逻辑了。obtainBufferreleaseBuffer可以去参考下http://thinks.me/2016/03/18/audiotrack_write/这篇文章,里面有谈到这两个函数的实现。
上面讲了那么多,其实无非就是检查下buffer有没有空间,如果有就去填充数据,如此反复。。只是实现起来没那么容易才有这么大片大片的代码。当然上面并没有包括控制流,全是数据流相关。跟audioflinger部分的交互,可以参考下图片。

audioflinger部分

这部分就是生产者与消费者模式中的消费者了,我们将从线程创建开始分析,再看AsyncCallbackThread的逻辑,不过我们只看跟普通线程的区别的部分。但是其实消费者的主要的逻辑确实在threadloop函数中,这个我们将只用图来描述,因为逻辑较为简单。

offloadThread创建

offloadThread线程的创建,其实依赖于audio_policy.conf中是否有配置,如果有配置,则才会有创建该线程的逻辑。
offload跟mixer线程的区别,其实主要是基本上没有mixer的处理,直接绕过audioflinger的采样率、format、采样精度、通道数等的转变,所以其实offload的逻辑是更简单的。

不过我们这篇重点在讲offload的callback的实现逻辑,直接引用PlaybackThread::readOutputParameters_l中的部分代码,这边有创建callback的线程

if ((mOutput->flags & AUDIO_OUTPUT_FLAG_NON_BLOCKING) &&
        (mOutput->stream->set_callback != NULL)) {
    if (mOutput->stream->set_callback(mOutput->stream,
                                  AudioFlinger::PlaybackThread::asyncCallback, this) == 0) {
        mUseAsyncWrite = true;
        mCallbackThread = new AudioFlinger::AsyncCallbackThread(this);
    }
}

从上面的函数中,可以看到判断依据是audio_policy.conf中compressoffload的节点中的flag是否带有non_blocking,如果有还需要确认底层是否支持callback的场景,如果支持的话,则告诉hal层,我们这边的回调函数是哪个,并且创造callback的线程来。这个线程在这个场景中至关重要,它会等到底层有空间的时候来唤醒offload线程去通知app来填充数据。

AsyncCallbackThread逻辑

我们直接来看它的实现代码吧。

下面这个代码来自PlaybackThread::threadLoop_write

if (mUseAsyncWrite) {
    // 将该值+2 | 1 保证末位为1
    mWriteAckSequence += 2;
    mWriteAckSequence |= 1;
    // 将该值同步到callback线程中。在该函数中,这个sequence将会被左移1位。
    mCallbackThread->setWriteBlocked(mWriteAckSequence);
}

bytesWritten = mOutput->write((char *)mSinkBuffer + offset, mBytesRemaining);
// 写完出来判断下写入的数据是否等于打算写入的值,如果不想等话,说明底层没有空间了。相等的话,则不需要进行block状态,可以再进行写操作。
if (mUseAsyncWrite &&
        ((bytesWritten < 0) || (bytesWritten == (ssize_t)mBytesRemaining))) {
    // 清除末尾为1
    mWriteAckSequence &= ~1;

    mCallbackThread->setWriteBlocked(mWriteAckSequence);
}

这个是setWriteBlocked的操作,这个函数在threadloop_write往hal层写数据的时候会被调用一次,退出write操作之后,如果打算写到底层的数据量跟真实写入的值相等的话,还会再调用一次这个函数。

void AudioFlinger::AsyncCallbackThread::setWriteBlocked(uint32_t sequence)
{
    Mutex::Autolock _l(mLock);
    mWriteAckSequence = sequence << 1;
}

这个是线程的逻辑:

bool AudioFlinger::AsyncCallbackThread::threadLoop()
{
    while (!exitPending()) {
        uint32_t writeAckSequence;
        uint32_t drainSequence;

        {
            Mutex::Autolock _l(mLock);
            // 如果mWriteAckSequence末位为1 或者 mDrainSequence为1 并且已经被唤醒
            // 当底层有空间的时候,这个线程就会被触发唤醒,继续走下面的逻辑
            while (!((mWriteAckSequence & 1) ||
                     (mDrainSequence & 1) ||
                     exitPending())) {
                mWaitWorkCV.wait(mLock);
            }
            // mWriteAckSequence、mDrainSequence 将末位进行清0,因为在threadloop_write的时候会将其+2 | 1 并且左移1位, 被唤醒之后,该值又被 |1 。
            writeAckSequence = mWriteAckSequence;
            // 对callback线程中的mWriteAckSequence、mDrainSequence进行末尾清零操作
            mWriteAckSequence &= ~1;
            drainSequence = mDrainSequence;
            mDrainSequence &= ~1;
        }
        {
            sp<AudioFlinger::PlaybackThread> playbackThread = mPlaybackThread.promote();
            if (playbackThread != 0) {
                // 如果之前block住了,那就进行resetWriteBlocked,这个将会触发offloadThread线程从wait进入到wake up的状态。
                if (writeAckSequence & 1) {
                    // 右移才是offloadThread中mWriteAckSequence的值
                    playbackThread->resetWriteBlocked(writeAckSequence >> 1);
                }
                if (drainSequence & 1) {
                    playbackThread->resetDraining(drainSequence >> 1);
                }
            }
        }
    }
    return false;
}

看下resetWriteBlocked的实现

void AudioFlinger::PlaybackThread::resetWriteBlocked(uint32_t sequence)
{
    Mutex::Autolock _l(mLock);
    // sequence这个值必须跟offloadThread中的mWriteAckSequence相等,才会触发唤醒操作。
    if ((mWriteAckSequence & 1) && (sequence == mWriteAckSequence)) {
        // 末尾清零,唤醒offloadThread线程
        mWriteAckSequence &= ~1;
        mWaitWorkCV.signal();
    }
}

上面的sequence巧妙的利用二进制来实现开关的操作,表示进入和退出的效果,并且保证sequence的值一直处在递增可以排重。

下图描述了offloadThread跟asynccallbackThread之间的调用逻辑:

offload write

下面在贴几张高通的文档中比较粗泛的图视:

framework、hal、kernel的框图:
offload write
offload的初始化:
offload write

offload的playback:
offload write
offload的seek:
offload write