DSP

高通audio offload学习

2019-07-13 10:54发布

高通audio offload学习|  

http://thinks.me/2016/09/13/audio_qcom_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 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 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 #Android #Audio #Offload