当前位置: 首页 > news >正文

安卓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)视频采集编码写入详细源码分析

相关文章:

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

文章目录 前言音频采集音频初始化AudioRecord 分析AudioSource 采集到音频 音频编码音频编码后数据处理MPEG4Writer写入音频编码后数据到文件MPEG4Writer::Track 取编码后的音频编数据结语 本文首发地址 https://blog.csdn.net/CSqingchen/article/details/134896808 最新更新地…...

2024年网络安全竞赛—网络安全事件分析应急响应解析(包含FLAG)

网络安全事件分析应急响应 目录 网络安全事件分析应急响应 解析如下:...

FineBI实战项目一(22):各省份订单个数及订单总额分析开发

点击新建组件&#xff0c;创建各省份订单个数及订单总额组件。 选择自定义图表&#xff0c;将province拖拽到横轴&#xff0c;将cnt和total拖拽到纵轴。 调节纵轴的为指标并列。 修改横轴和纵轴的标题。 修改柱状图样式&#xff1a; 将组件拖拽到仪表板。 结果如下&#xff1a;…...

2024.1.16 调用tinyspline样条曲线拟合库时报 stack smashing detected,CMakeLists.txt中屏蔽该异常

在函数中调用第三方库api拟合样条曲线&#xff0c;函数中一切正常&#xff0c;可以打印所有数组变量&#xff0c;重复执行该函数&#xff0c;某一次函数return时报 stack smashing deteced &#xff08;unknown&#xff09; &#xff0c;原因可能是第三方库内部的函数有栈溢出风…...

Leetcode202快乐数(java实现)

今天分享的题目是快乐数&#xff1a; 快乐数的定义如下&#xff1a; 快乐数&#xff08;Happy Number&#xff09;是指一个正整数&#xff0c;将其替换为各个位上数字的平方和&#xff0c;重复这个过程直到最后得到的结果为1&#xff0c;或者无限循环但不包含1。如果最终结果为…...

50天精通Golang(第13天)

反射reflect 一、引入 先看官方Doc中Rob Pike给出的关于反射的定义&#xff1a; Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confus…...

大数据 - Doris系列《三》- 数据表设计之表的基本概念

目录 &#x1f436;3.1 字段类型 &#x1f436;3.2 表的基本概念 3.2.1 Row & Column 3.2.2 分区与分桶 &#x1f959;3.2.2.1 Partition 1. Range 分区 2. List 分区 进阶&#xff1a;复合分区与单分区的选择 3.2.3 PROPERTIES &#x1f959;3.2.3.1 分片副本数 &#x1f…...

数据库mysql no.3

1.排序查询 order by 排序列表 【asc/desc】 排序列表&#xff1a;可以是单个字段、多个字段、表达式、函数、别名。 asc 升序 desc 降序 如果没有写那就是默认升序 2.常见函数 select 函数名&#xff08;&#xff09;&#xff1b; 定义&#xff1a;函…...

数据结构实战:变位词侦测

文章目录 一、实战概述二、实战步骤&#xff08;一&#xff09;逐个比较法1、编写源程序2、代码解释说明&#xff08;1&#xff09;函数逻辑解释&#xff08;2&#xff09;主程序部分 3、运行程序&#xff0c;查看结果4、计算时间复杂度 &#xff08;二&#xff09;排序比较法1…...

C++核心编程之类和对象---C++面向对象的三大特性--多态

目录 一、多态 1. 多态的概念 2.多态的分类&#xff1a; 1. 静态多态&#xff1a; 2. 动态多态&#xff1a; 3.静态多态和动态多态的区别&#xff1a; 4.动态多态需要满足的条件&#xff1a; 4.1重写的概念&#xff1a; 4.2动态多态的调用&#xff1a; 二、多态 三、多…...

基于PyQT的图片批处理系统

项目背景&#xff1a; 随着数字摄影技术的普及&#xff0c;人们拍摄和处理大量图片的需求也越来越高。为了提高效率&#xff0c;开发一个基于 PyQt 的图片批处理系统是很有意义的。该系统可以提供一系列图像增强、滤波、水印、翻转、放大缩小、旋转等功能&#xff0c;使用户能够…...

vscode文件配置

lanuch.json {"version": "0.2.0","configurations": [{"name": "(gdb) 启动","type": "cppdbg","request": "launch",// "program": "输入程序名称&#xff0c;例…...

C++学习笔记——SLT六大组件及头文件

目录 一、C中STL&#xff08;Standard Template Library&#xff09; 二、 Gun源代码开发精神 三、 实现版本 四、GNU C库的头文件分布 bits目录 ext目录 backward目录 iostream目录 stdexcept目录 string目录 上一篇文章&#xff1a; C标准模板库&#xff08;STL&am…...

Spring之AOP源码(二)

书接上文 文章目录 一、简介1. 前文回顾2. 知识点补充 二、ProxyFactory源码分析1. ProxyFactory2. JdkDynamicAopProxy3. ObjenesisCglibAopProxy 三、 Spring AOP源码分析 一、简介 1. 前文回顾 前面我们已经介绍了AOP的基本使用方法以及基本原理&#xff0c;但是还没有涉…...

VS code console.log快捷键设置 :console.log(‘n>>>‘,n)

vscode设置log快捷显示&#xff1a; 一、打开 VS Code&#xff0c;并进入菜单栏选择 “文件”&#xff08;File&#xff09;-> “首选项”&#xff08;Preferences&#xff09;-> “用户代码片段”&#xff08;User Snippets&#xff09;。 二、在弹出的下拉菜单中选择 …...

ZooKeeper 简介

1、概念介绍 ZooKeeper 是一个开放源码的分布式应用程序协调服务&#xff0c;为分布式应用提供一致性服务的软件&#xff0c;由雅虎创建&#xff0c;是 Google Chubby 的开源实现&#xff0c;是 Apache 的子项目&#xff0c;之前是 Hadoop 项目的一部分&#xff0c;使用 Java …...

rke2 Online Deploy Rancher v2.8.0 latest (helm 在线部署 rancher v2.8.0)

文章目录 1. 简介2. 预备条件3. 安装 helm4. 安装 cert-manager4.1 yaml 安装4.2 helm 安装 5. 安装 rancher6. 验证7. 界面预览 1. 简介 Rancher 是一个 Kubernetes 管理工具&#xff0c;让你能在任何地方和任何提供商上部署和运行集群。 Rancher 可以创建来自 Kubernetes 托…...

k8s实战从入门到上天系列第一篇:K8s微服务实战内容开篇介绍

前言 我们使用开源ruoyi微服务基本使用&#xff0c;基于基本的微服务实践。我们来讲解k8s的实战内容。 第一章&#xff1a;开源ruoyi微服务简介基本使用 第二章&#xff1a;k8s基本知识回顾、k3s集群搭建和基本使用 第三章&#xff1a;微服务镜像构建 第四章&#xff1a;中间件…...

统一网关 Gateway【微服务】

文章目录 1. 前言2. 搭建网关服务3. 路由断言工厂4. 路由过滤器4.1 普通过滤器4.2 全局过滤器4.3 过滤器执行顺序 5. 跨域问题处理 1. 前言 通过前面的学习我们知道&#xff0c;通过 Feign 就可以向指定的微服务发起 http 请求&#xff0c;完成远程调用。但是这里有一个问题&am…...

【征服redis1】基础数据类型详解和应用案例

博客计划 &#xff0c;我们从redis开始&#xff0c;主要是因为这一块内容的重要性不亚于数据库&#xff0c;但是很多人往往对redis的问题感到陌生&#xff0c;所以我们先来研究一下。 本篇&#xff0c;我们先看一下redis的基础数据类型详解和应用案例。 1.redis概述 以mysql为…...

vscode里如何用git

打开vs终端执行如下&#xff1a; 1 初始化 Git 仓库&#xff08;如果尚未初始化&#xff09; git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解

【关注我&#xff0c;后续持续新增专题博文&#xff0c;谢谢&#xff01;&#xff01;&#xff01;】 上一篇我们讲了&#xff1a; 这一篇我们开始讲&#xff1a; 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下&#xff1a; 一、场景操作步骤 操作步…...

centos 7 部署awstats 网站访问检测

一、基础环境准备&#xff08;两种安装方式都要做&#xff09; bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats&#xff0…...

前端导出带有合并单元格的列表

// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

家政维修平台实战20:权限设计

目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系&#xff0c;主要是分成几个表&#xff0c;用户表我们是记录用户的基础信息&#xff0c;包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题&#xff0c;不同的角色&#xf…...

Rapidio门铃消息FIFO溢出机制

关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系&#xff0c;以下是深入解析&#xff1a; 门铃FIFO溢出的本质 在RapidIO系统中&#xff0c;门铃消息FIFO是硬件控制器内部的缓冲区&#xff0c;用于临时存储接收到的门铃消息&#xff08;Doorbell Message&#xff09;。…...

均衡后的SNRSINR

本文主要摘自参考文献中的前两篇&#xff0c;相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程&#xff0c;其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt​ 根发送天线&#xff0c; n r n_r nr​ 根接收天线的 MIMO 系…...

ip子接口配置及删除

配置永久生效的子接口&#xff0c;2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...