安卓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> ¬ify) {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 音频采集、编码、写入文件详细源码分析。
用一幅图概括如下
希望对你有所帮助。如果你在使用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):各省份订单个数及订单总额分析开发
点击新建组件,创建各省份订单个数及订单总额组件。 选择自定义图表,将province拖拽到横轴,将cnt和total拖拽到纵轴。 调节纵轴的为指标并列。 修改横轴和纵轴的标题。 修改柱状图样式: 将组件拖拽到仪表板。 结果如下:…...
2024.1.16 调用tinyspline样条曲线拟合库时报 stack smashing detected,CMakeLists.txt中屏蔽该异常
在函数中调用第三方库api拟合样条曲线,函数中一切正常,可以打印所有数组变量,重复执行该函数,某一次函数return时报 stack smashing deteced (unknown) ,原因可能是第三方库内部的函数有栈溢出风…...

Leetcode202快乐数(java实现)
今天分享的题目是快乐数: 快乐数的定义如下: 快乐数(Happy Number)是指一个正整数,将其替换为各个位上数字的平方和,重复这个过程直到最后得到的结果为1,或者无限循环但不包含1。如果最终结果为…...
50天精通Golang(第13天)
反射reflect 一、引入 先看官方Doc中Rob Pike给出的关于反射的定义: 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系列《三》- 数据表设计之表的基本概念
目录 🐶3.1 字段类型 🐶3.2 表的基本概念 3.2.1 Row & Column 3.2.2 分区与分桶 🥙3.2.2.1 Partition 1. Range 分区 2. List 分区 进阶:复合分区与单分区的选择 3.2.3 PROPERTIES 🥙3.2.3.1 分片副本数 …...
数据库mysql no.3
1.排序查询 order by 排序列表 【asc/desc】 排序列表:可以是单个字段、多个字段、表达式、函数、别名。 asc 升序 desc 降序 如果没有写那就是默认升序 2.常见函数 select 函数名(); 定义:函…...

数据结构实战:变位词侦测
文章目录 一、实战概述二、实战步骤(一)逐个比较法1、编写源程序2、代码解释说明(1)函数逻辑解释(2)主程序部分 3、运行程序,查看结果4、计算时间复杂度 (二)排序比较法1…...

C++核心编程之类和对象---C++面向对象的三大特性--多态
目录 一、多态 1. 多态的概念 2.多态的分类: 1. 静态多态: 2. 动态多态: 3.静态多态和动态多态的区别: 4.动态多态需要满足的条件: 4.1重写的概念: 4.2动态多态的调用: 二、多态 三、多…...

基于PyQT的图片批处理系统
项目背景: 随着数字摄影技术的普及,人们拍摄和处理大量图片的需求也越来越高。为了提高效率,开发一个基于 PyQt 的图片批处理系统是很有意义的。该系统可以提供一系列图像增强、滤波、水印、翻转、放大缩小、旋转等功能,使用户能够…...
vscode文件配置
lanuch.json {"version": "0.2.0","configurations": [{"name": "(gdb) 启动","type": "cppdbg","request": "launch",// "program": "输入程序名称,例…...

C++学习笔记——SLT六大组件及头文件
目录 一、C中STL(Standard Template Library) 二、 Gun源代码开发精神 三、 实现版本 四、GNU C库的头文件分布 bits目录 ext目录 backward目录 iostream目录 stdexcept目录 string目录 上一篇文章: C标准模板库(STL&am…...

Spring之AOP源码(二)
书接上文 文章目录 一、简介1. 前文回顾2. 知识点补充 二、ProxyFactory源码分析1. ProxyFactory2. JdkDynamicAopProxy3. ObjenesisCglibAopProxy 三、 Spring AOP源码分析 一、简介 1. 前文回顾 前面我们已经介绍了AOP的基本使用方法以及基本原理,但是还没有涉…...
VS code console.log快捷键设置 :console.log(‘n>>>‘,n)
vscode设置log快捷显示: 一、打开 VS Code,并进入菜单栏选择 “文件”(File)-> “首选项”(Preferences)-> “用户代码片段”(User Snippets)。 二、在弹出的下拉菜单中选择 …...

ZooKeeper 简介
1、概念介绍 ZooKeeper 是一个开放源码的分布式应用程序协调服务,为分布式应用提供一致性服务的软件,由雅虎创建,是 Google Chubby 的开源实现,是 Apache 的子项目,之前是 Hadoop 项目的一部分,使用 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 管理工具,让你能在任何地方和任何提供商上部署和运行集群。 Rancher 可以创建来自 Kubernetes 托…...
k8s实战从入门到上天系列第一篇:K8s微服务实战内容开篇介绍
前言 我们使用开源ruoyi微服务基本使用,基于基本的微服务实践。我们来讲解k8s的实战内容。 第一章:开源ruoyi微服务简介基本使用 第二章:k8s基本知识回顾、k3s集群搭建和基本使用 第三章:微服务镜像构建 第四章:中间件…...

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

【征服redis1】基础数据类型详解和应用案例
博客计划 ,我们从redis开始,主要是因为这一块内容的重要性不亚于数据库,但是很多人往往对redis的问题感到陌生,所以我们先来研究一下。 本篇,我们先看一下redis的基础数据类型详解和应用案例。 1.redis概述 以mysql为…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...

centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...

前端导出带有合并单元格的列表
// 导出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)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...

均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...