安卓MediaRecorder(3)音频采集编码写入详细源码分析

文章目录

    • 前言
    • 音频采集
      • 音频初始化
      • AudioRecord 分析
      • AudioSource 采集到音频
    • 音频编码
    • 音频编码后数据处理
    • MPEG4Writer写入音频编码后数据到文件
    • MPEG4Writer::Track 取编码后的音频编数据
    • 结语

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/134896808
最新更新地址 https://gitee.com/chenjim/chenjimblog

前言

通过安卓MediaRecorder(2)录制源码分析,我们知道 MediaRecorder 相关接口是在 StagefrightRecorder.cpp 中实现,本文进一步分析音频采集、编码、写入文件详细流程。

音频采集

音频初始化

通过前文,我们知道 setupAudioEncoder 在 setupMPEG4orWEBMRecording 中初始化,相关源码如下

// frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::setupAudioEncoder() {sp<MediaCodecSource> audioEncoder = createAudioSource();return OK;
}
sp<MediaCodecSource> StagefrightRecorder::createAudioSource() {...// 通过 AVFactory 工厂创建 AudioSource,并初始化 sp<AudioSource> audioSource = AVFactory::get()->createAudioSource(&attr,mAttributionSource,sourceSampleRate,mAudioChannels,mSampleRate,mSelectedDeviceId,mSelectedMicDirection,mSelectedMicFieldDimension);}

那 AudioSource 是如何初始化的呢

// frameworks/av/media/libstagefright/AudioSource.cpp
void AudioSource::set(const audio_attributes_t *attr, const AttributionSourceState& attributionSource,uint32_t sampleRate, uint32_t channelCount, uint32_t outSampleRate,audio_port_handle_t selectedDeviceId,audio_microphone_direction_t selectedMicDirection,float selectedMicFieldDimension)
{...// 构造了 一个 AudioRecord cpp 对象  mRecord = new AudioRecord(AUDIO_SOURCE_DEFAULT, sampleRate, AUDIO_FORMAT_PCM_16_BIT,audio_channel_in_mask_from_count(channelCount),attributionSource,(size_t) (bufCount * frameCount),// 采集的音频数据回调 wp<AudioRecord::IAudioRecordCallback>{this},frameCount /*notificationFrames*/,AUDIO_SESSION_ALLOCATE,AudioRecord::TRANSFER_DEFAULT,AUDIO_INPUT_FLAG_NONE,attr,selectedDeviceId,selectedMicDirection,selectedMicFieldDimension);...
}

AudioRecord.java 底层的实现也是 AudioSource.cpp
AudioRecord 主要是负责从麦克风设备采集音频 PCM 帧

AudioRecord 分析

// frameworks/av/media/libaudioclient/AudioRecord.cpp
status_t AudioRecord::set(...) {...if (mCallback != nullptr) {// 启动录制的线程 mAudioRecordThread = new AudioRecordThread(*this);mAudioRecordThread->run("AudioRecord", ANDROID_PRIORITY_AUDIO);}...
}
bool AudioRecord::AudioRecordThread::threadLoop() {...nsecs_t ns =  mReceiver.processAudioBuffer();...
}nsecs_t AudioRecord::processAudioBuffer() {... // 回调 AudioRecord::IAudioRecordCallback if (newOverrun) {callback->onOverrun();}if (markerReached) {callback->onMarker(markerPosition.value());}while (newPosCount > 0) {callback->onNewPos(newPosition.value());newPosition += updatePeriod;newPosCount--;}if (mObservedSequence != sequence) {mObservedSequence = sequence;callback->onNewIAudioRecord();}while (mRemainingFrames > 0) {// 获取 audioBuffer status_t err = obtainBuffer(&audioBuffer, requested, NULL, &nonContig);// 回调 取到的 buffer 到 AudioSource 中 onMoreData  const size_t readSize = callback->onMoreData(*buffer);// 释放 buffer releaseBuffer(&audioBuffer);}
}

AudioSource 采集到音频

// frameworks/av/media/libstagefright/AudioSource.cpp
size_t AudioSource::onMoreData(const AudioRecord::Buffer& audioBuffer) { ...// 将AudioRecord::Buffer 放入 MediaBufferMediaBuffer *buffer = new MediaBuffer(audioBuffer.size());memcpy((uint8_t *) buffer->data(),audioBuffer.data(), audioBuffer.size());buffer->set_range(0, audioBuffer.size());// 将 buffer 放入缓存queueInputBuffer_l(buffer, timeUs);return audioBuffer.size();
}
void AudioSource::queueInputBuffer_l(MediaBuffer *buffer, int64_t timeUs) {...// 将 buffer 放入缓存 mBuffersReceived 中mBuffersReceived.push_back(buffer);mFrameAvailableCondition.signal();
}// 如下接口可以读取采集到的 buffer
status_t AudioSource::read(MediaBufferBase **out, const ReadOptions * /* options */) {...MediaBuffer *buffer = *mBuffersReceived.begin();mBuffersReceived.erase(mBuffersReceived.begin());buffer->setObserver(this);...*out = buffer;
}

音频编码

编码器创建如下

sp<MediaCodecSource> StagefrightRecorder::createAudioSource() {sp<MediaCodecSource> audioEncoder = MediaCodecSource::Create(mLooper, format, audioSource);
}
// MediaCodecSource 构造如下  
MediaCodecSource::MediaCodecSource(const sp<ALooper> &looper,const sp<AMessage> &outputFormat,const sp<MediaSource> &source,const sp<PersistentSurface> &persistentSurface,uint32_t flags){if (!(mFlags & FLAG_USE_SURFACE_INPUT)) {// 将 AudioSource 放入 Puller 中mPuller = new Puller(source);}
}

MediaCodecSource::start 发送 kWhatStart 消息

status_t MediaCodecSource::start(MetaData* params) {sp<AMessage> msg = new AMessage(kWhatStart, mReflector);msg->setObject("meta", params);// 发消息 kWhatStart 到 MediaCodecSource::onMessageReceived // 进而传递到 MediaCodecSource::onStart return postSynchronouslyAndReturnError(msg);
}
void MediaCodecSource::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatStart: {sp<AMessage> response = new AMessage;// 调用 MediaCodecSource::onStart response->setInt32("err", onStart(params));response->postReply(replyID);}}
}
status_t MediaCodecSource::onStart(MetaData *params) {...// 创建 kWhatPullerNotify 消息,传入  MediaCodecSource::Puller::start sp<AMessage> notify = new AMessage(kWhatPullerNotify, mReflector);err = mPuller->start(meta.get(), notify);
}

MediaCodecSource::Puller::start 流程如下

status_t MediaCodecSource::Puller::start(const sp<MetaData> &meta, const sp<AMessage> &notify) {mNotify = notify;// 发送 kWhatStart 消息 到 MediaCodecSource::Puller::onMessageReceivedsp<AMessage> msg = new AMessage(kWhatStart, this);msg->setObject("meta", meta);return postSynchronouslyAndReturnError(msg);
}
void MediaCodecSource::Puller::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatStart: {// start后,就开始 pull schedulePull();}case kWhatPull:{// 通过上文的 AudioSource::read 读取采集到的数据status_t err = mSource->read(&mbuf);// 将读取到的 mbuf 放入队列 queue->pushBuffer(mbuf);if (mbuf != NULL) {// 送到 MediaCodecSource::onMessageReceived, 通知编码器 pull 到数据mNotify->post();// 继续 pull msg->post();} else {// 结束 EndOfStream handleEOS();}}
}

MediaCodecSource::Puller 读取到数据后,mNotify 发消息 kWhatPullerNotify 通知编码

void MediaCodecSource::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatPullerNotify:{...// 收到 通知,送去编码feedEncoderInputBuffers();}}
}
status_t MediaCodecSource::feedEncoderInputBuffers() {// 取数据编码while (!mAvailEncoderInputIndices.empty() && mPuller->readBuffer(&mbuf)) {...// inbuf 送到编码器status_t err = mEncoder->getInputBuffer(bufferIndex, &inbuf);...// 编码status_t err = mEncoder->queueInputBuffer(bufferIndex, 0, size, timeUs, flags);}
}

音频编码后数据处理

在创建编码器时,把 mEncoderActivityNotify 设置到编码器的 Callback,编码器的消息会通过 kWhatEncoderActivity 发送出来

status_t MediaCodecSource::initEncoder() {...mEncoderActivityNotify = new AMessage(kWhatEncoderActivity, mReflector);mEncoder->setCallback(mEncoderActivityNotify);...
}

当编码完成、状态变化,会收到 kWhatEncoderActivity 消息通知

void MediaCodecSource::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatEncoderActivity:{if (cbID == MediaCodec::CB_INPUT_AVAILABLE) {// 输入不可用,继续给编码器送输入} else if (cbID == MediaCodec::CB_OUTPUT_FORMAT_CHANGED) {// 输出格式变化} else if (cbID == MediaCodec::CB_OUTPUT_AVAILABLE) {// 正常的输出数据// 获取编码器额输出status_t err = mEncoder->getOutputBuffer(index, &outbuf);// 将输出 buf 转  MediaBufferMediaBuffer *mbuf = new MediaBuffer(outbuf->size());// 提取 MetaDatasp<MetaData> meta = new MetaData(mbuf->meta_data());...// 将 编码数据 outbuf 填充到 mbufmemcpy(mbuf->data(), outbuf->data(), outbuf->size());// 将编码后的数据添加到队列output->mBufferQueue.push_back(mbuf);} else if (cbID == MediaCodec::CB_ERROR) {// ERROR 异常,退出signalEOS(err);}}}
}

当需要数据时,从输出队列取数据即可

status_t MediaCodecSource::read(MediaBufferBase** buffer, const ReadOptions* /* options */) {Mutexed<Output>::Locked output(mOutput);*buffer = NULL;while (output->mBufferQueue.size() == 0 && !output->mEncoderReachedEOS) {output.waitForCondition(output->mCond);}if (!output->mEncoderReachedEOS) {*buffer = *output->mBufferQueue.begin();output->mBufferQueue.erase(output->mBufferQueue.begin());return OK;}return output->mErrorCode;
}

MPEG4Writer写入音频编码后数据到文件

通过如下源码,我们知道了 MPEG4Writer 创建和写入线程启动

status_t StagefrightRecorder::setupMPEG4orWEBMRecording() {...writer = mp4writer = new MPEG4Writer(mOutputFd);
}
status_t StagefrightRecorder::start() {...status = mWriter->start(meta.get());
}
status_t MPEG4Writer::start(MetaData *param) {...err = startWriterThread();...// 这个 startTracks 主要为 MPEG4Writer::Track 做准备 err = startTracks(param);
}
status_t MPEG4Writer::startWriterThread() {mDone = false;mIsFirstChunk = true;mDriftTimeUs = 0;// 将 音、视频 Track 添加到 mChunkInfosfor (List<Track *>::iterator it = mTracks.begin();it != mTracks.end(); ++it) {ChunkInfo info;info.mTrack = *it;info.mPrevChunkTimestampUs = 0;info.mMaxInterChunkDurUs = 0;mChunkInfos.push_back(info);}...// 启动线程执行 ThreadWrapper pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);pthread_create(&mThread, &attr, ThreadWrapper, this);pthread_attr_destroy(&attr);return OK;
}
void *MPEG4Writer::ThreadWrapper(void *me) {MPEG4Writer *writer = static_cast<MPEG4Writer *>(me);// 最终执行的是 threadFunc()writer->threadFunc();return NULL;
}

写入线程开启后,一直循环,无数据时等待

void MPEG4Writer::threadFunc() {Mutex::Autolock autoLock(mLock);while (!mDone) {Chunk chunk;bool chunkFound = false;// findChunkToWrite 从 mChunkInfos 找到需要写入的 Chunkwhile (!mDone && !(chunkFound = findChunkToWrite(&chunk))) {mChunkReadyCondition.wait(mLock);}// 在实时记录模式下,写时不按顺序持有锁, 减少媒体跟踪线程的阻塞时间。// 否则,保持锁,直到现有的块被写入文件。if (chunkFound) {if (mIsRealTimeRecording) {mLock.unlock();}// 写入 Chunk writeChunkToFile(&chunk);if (mIsRealTimeRecording) {mLock.lock();}}}// 写入所有内存writeAllChunks();
}

写入到文件是在 writeChunkToFile 中完成

void MPEG4Writer::writeChunkToFile(Chunk* chunk) {while (!chunk->mSamples.empty()) {// 取一个 MediaBuffer List<MediaBuffer *>::iterator it = chunk->mSamples.begin();...// 写入 MediaBuffer off64_t offset = addSample_l(*it, usePrefix, tiffHdrOffset, &bytesWritten);...}// 写入后清空chunk->mSamples.clear();
}
off64_t MPEG4Writer::addSample_l(MediaBuffer *buffer, bool usePrefix,uint32_t tiffHdrOffset, size_t *bytesWritten) {...writeOrPostError(mFd, (const uint8_t*)buffer->data() + buffer->range_offset(),buffer->range_length());
}
void MPEG4Writer::writeOrPostError(int fd, const void* buf, size_t count) {...// 真正的写入 buf ssize_t bytesWritten = ::write(fd, buf, count);...// IO 异常时 抛出 ,通过消息传递到上层  sp<AMessage> msg = new AMessage(kWhatIOError, mReflector);msg->setInt32("err", ERROR_IO);
}

MPEG4Writer::Track 取编码后的音频编数据

MPEG4Writer::Track 启动源码如下

status_t MPEG4Writer::startTracks(MetaData *params) {...for (List<Track *>::iterator it = mTracks.begin();it != mTracks.end(); ++it) {// MPEG4Writer::Track  start status_t err = (*it)->start(params);...}return OK;
}
status_t MPEG4Writer::Track::start(MetaData *params) {...// 启动线程执行 ThreadWrapper pthread_create(&mThread, &attr, ThreadWrapper, this);
}
void *MPEG4Writer::Track::ThreadWrapper(void *me) {Track *track = static_cast<Track *>(me);status_t err = track->threadEntry();return (void *)(uintptr_t)err;
}

MPEG4Writer::Track::threadEntry 读取编码后的数据

status_t MPEG4Writer::Track::threadEntry() {// mSource->read 也就是 上文 MediaCodecSource::read ,一直不停的读取数据到 buffer  MediaBufferBase *buffer;while (!mDone && (err = mSource->read(&buffer)) == OK && buffer != NULL) {...// 将 buffer 转为 MediaBuffer MediaBuffer *copy = new MediaBuffer(buffer->range_length());if (sampleFileOffset != -1) {copy->meta_data().setInt64(kKeySampleFileOffset, sampleFileOffset);} else {memcpy(copy->data(), (uint8_t*)buffer->data() + buffer->range_offset(),buffer->range_length());}...// 将 copy 放入队列 mChunkSamplesmChunkSamples.push_back(copy);...// 将 mChunkSamples 转 为 ChunkbufferChunk(timestampUs);}
}
void MPEG4Writer::Track::bufferChunk(int64_t timestampUs) {Chunk chunk(this, timestampUs, mChunkSamples);// 也就是  MPEG4Writer::bufferChunk mOwner->bufferChunk(chunk);mChunkSamples.clear();
}
void MPEG4Writer::bufferChunk(const Chunk& chunk) {Mutex::Autolock autolock(mLock);for (List<ChunkInfo>::iterator it = mChunkInfos.begin();it != mChunkInfos.end(); ++it) {if (chunk.mTrack == it->mTrack) {// 将 Chunk 放入 ChunkInfo.mChunks 中,it->mChunks.push_back(chunk);// 数据准备好了,通知 mChunkReadyCondition.wait 继续执行// 进而 由 findChunkToWrite 读取写入文件  mChunkReadyCondition.signal();return;}}
}

结语

到这里,已经完成了 MediaRecorder 音频采集、编码、写入文件详细源码分析。
用一幅图概括如下

MPEG4Writer::Track
+读取编码后内容()
AudioSource
+采集音频()
MediaCodecSource
+编码()
MPEG4Writer
+写入编码后内容()

希望对你有所帮助。如果你在使用MediaRecorder的过程中遇到了其他问题,欢迎留言讨论。
如果你觉得本文还不错,可以点赞+收藏。


相关文章
安卓MediaRecorder(1)录制音频的详细使用
安卓MediaRecorder(2)录制源码分析
安卓MediaRecorder(3)音频采集编码写入详细源码分析
安卓MediaRecorder(4)视频采集编码写入详细源码分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/620415.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flask 小程序菜品搜索

mina/pages/food/index.wxml <!--index.wxml--> <!--1px 750/320 2.34rpx;--> <view class"container"><!--轮播图--><view class"swiper-container"><swiper class"swiper_box" autoplay"{{autoplay}…

直播预告丨看零售场,如何玩转 MaaS

今年&#xff0c;有一个被频繁提及的词是MaaS 这类工具正在帮助千行百业实现大模型落地产业 在零售场&#xff0c;特别是像京东这样拥有超高并发、超复杂协同的电商场内 也沉淀出了一套通用的AI基础设施——九数算法中台 从提升客户服务体验、平台效率出发&#xff0c;训练各…

Vue:调用浏览器数据库

在前一段时间写项目的时候&#xff0c;需要本地存储大量数据&#xff0c;需要在客户端进行数据存储、离线访问以及数据同步等&#xff0c;对本地数据的储存和管理非常重要。因此考虑使用了IndexedDB&#xff0c;但是接使用 IndexedDB API &#xff0c;非常麻烦&#xff0c;需要…

学习笔记-python文件基本操作

1.文件的基本操作 open()打开函数 语法 : open(name,mode) name&#xff1a;是要打开的目标文件名的字符串(可以包含文件所在的具体路径)。 mode&#xff1a;设置打开文件的模式(访问模式)&#xff1a;只读、写入、追加等。 # 打开文件open(): 如果报FileNotFoundError,文件路…

【Python】数据可视化--基于TMDB_5000_Movie数据集

一、数据准备 tmdb_5000_movie数据集下载 二、数据预处理 观察数据集合情况 import pandas as pd import ast import warnings warnings.filterwarnings(ignore) # 加载数据集 df pd.read_csv(tmdb_5000_movies.csv) # 查看数据集信息 print(df.info()) 由于原数据集包含的…

Jenkins集成Sonar Qube

下载插件 重启Jenkins 容器 sonarqube 使用令牌 Jenkins 配置 重新构建

小程序基础学习(多插槽)

先创建插槽 定义多插槽的每一个插槽的属性 在js文件中启用多插槽 在页面使用多插槽 组件代码 <!--components/my-slots/my-slots.wxml--><view class"container"><view class"left"> <slot name"left" ></slot>&…

YOLOv8改进 | 注意力篇 | 实现级联群体注意力机制CGAttention (全网首发)

一、本文介绍 本文给大家带来的改进机制是实现级联群体注意力机制CascadedGroupAttention,其主要思想为增强输入到注意力头的特征的多样性。与以前的自注意力不同,它为每个头提供不同的输入分割,并跨头级联输出特征。这种方法不仅减少了多头注意力中的计算冗余,而且通过增…

八爪鱼拉拉手

欢迎来到程序小院 八爪鱼拉拉手 玩法&#xff1a;点击鼠标左键拖动移动八爪鱼&#xff0c;当他的手很忙的时候他会很高兴&#xff0c; 不同关卡不同的八爪鱼的位置摆放&#xff0c;快去闯关吧^^。开始游戏https://www.ormcc.com/play/gameStart/248 html <div id"gam…

GCC工具源码编译

文章目录 背景一、下载源码二、编译前依赖准备2.1 相关工具依赖2.2 相关lib&#xff08;gmp/ mpfr /mpc&#xff09;依赖2.2.1 lib源码下载2.2.2 lib源码编译 三、编译GCC3.1 编译3.2 链接 四、报错处理 背景 日常可能涉及到系统里自带GCC版本与被编译源码存在不兼容&#xff…

【MySQL】巧用 Dense_Rank 窗口函数

力扣题 1、题目地址 2084. 为订单类型为 0 的客户删除类型为 1 的订单 2、模拟表 活动表&#xff1a;Orders Column NameTypeorder_idintcustomer_idintorder_typeint order_id是此表的主键列。此表的每一行都表示订单的ID、订购该订单的客户的ID以及订单类型。订单可以是…

大模型背景下计算机视觉年终思考小结(一)

1. 引言 在过去的十年里&#xff0c;出现了许多涉及计算机视觉的项目&#xff0c;举例如下&#xff1a; 使用射线图像和其他医学图像领域的医学诊断应用使用卫星图像分析建筑物和土地利用率相关应用各种环境下的目标检测和跟踪&#xff0c;如交通流统计、自然环境垃圾检测估计…

JavaScript系列——Proxy(代理)

文章目录 概要Proxy 语法handler 对象的方法Proxy 示例常用handler 对象的方法的参数handler.get()语法示例 handler.set()语法示例 使用场景验证值修正及附加属性 小结 概要 Proxy 用于创建一个对象的代理&#xff0c;将对原对象上的操作&#xff08;属性获取、赋值、函数调用…

【SSM框架】初识Spring

初识Spring Spring家族 Spring发展到今天已经形成了一种开发的生态圈&#xff0c;Spring提供了若千个项目&#xff0c;每个项目用于完成特定的功能 ✅Spring Framework&#xff08;底层框架&#xff09;Spring Boot&#xff08;提高开发速度&#xff09;Spring Cloud&#xf…

第1课 ROS 系统介绍

1.ROS操作系统介绍 在学习ROS 系统前&#xff0c;我们需要先了解操作系统的定义。操作系统&#xff0c;顾名思义&#xff0c;即提供部分软件和硬件的接口&#xff0c;以供用户直接使用。因此&#xff0c;针对不同的平台、不同的功能&#xff0c;需要采用不同的操作系统来完成底…

C++ 具名要求-全库范围的概念 - 函数对象 (FunctionObject) ,对于不同输入值产生相同输出具有很低概率-散列(hash)

此页面中列出的具名要求&#xff0c;是 C 标准的规范性文本中使用的具名要求&#xff0c;用于定义标准库的期待。 某些具名要求在 C20 中正在以概念语言特性进行形式化。在那之前&#xff0c;确保以满足这些要求的模板实参实例化标准库模板是程序员的重担。若不这么做&#xf…

智能导诊-医院信息化建设标准

智能导诊系统主要依赖于自然语言处理和机器学习等技术。患者可以通过语音、文字等方式描述病情&#xff0c;系统通过自然语言处理技术对病情进行语义分析和理解。随后&#xff0c;机器学习算法对患者的症状和病情进行推理&#xff0c;结合已有的疾病知识库&#xff0c;为患者提…

canvas设置图形图案、文字图案

查看专栏目录 canvas示例教程100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

强化学习应用(二):基于Q-learning的物流配送路径规划研究(提供Python代码)

一、Q-learning算法简介 Q-learning是一种强化学习算法&#xff0c;用于解决基于马尔可夫决策过程&#xff08;MDP&#xff09;的问题。它通过学习一个值函数来指导智能体在环境中做出决策&#xff0c;以最大化累积奖励。 Q-learning算法的核心思想是使用一个Q值函数来估计每…

AI大模型学习笔记一

一、商业观点&#xff1a;企业借助大模型获得业务增长可能 二、底层原理&#xff1a;transformer 1&#xff09;备注 ①下面每个步骤都是自回归的过程&#xff08;aotu-regressive&#xff09;&#xff1a;已输出内容的每个字作为输入&#xff0c;一起生成下一个字 ②合起来就…