纯血鸿蒙 AudioRenderer+AudioCapturer+RingBuffer 实现麦克风采集+发声

总共两个类,放到代码里,就可以快速完成K歌的效果,但应用层这么做延迟是比较高的,只是做一个分享。

类代码

import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AudioBufferFlow, AudioRingBuffer } from './AudioRingBuffer';
import { abilityAccessCtrl, PermissionRequestResult, Permissions } from '@kit.AbilityKit';
import { fileIo, WriteOptions } from '@kit.CoreFileKit';export class AudioRenderUtil {private readonly tag = "AudioRenderUtil";private audioRenderer?: audio.AudioRenderer;/**如果需要调试,存储一份 pcm,可以把这里设置 true,拉文件出来,命令看官方文档 */private readonly withWrite = false;private targetFile?: fileIo.File;private bufferSize = 0;/** RingBuffer 环缓冲区 */private ringBuffer: AudioRingBuffer;constructor(context: Context,streamInfo: audio.AudioStreamInfo,renderInfo: audio.AudioRendererInfo,) {this.ringBuffer = new AudioRingBuffer(streamInfo, 0.8, 0.2);const option: audio.AudioRendererOptions = {streamInfo: streamInfo,rendererInfo: renderInfo}LsLog.i(this.tag, `create by ${JSON.stringify(option)}`);if (this.withWrite) {try {this.targetFile = fileIo.openSync(context.cacheDir + `/renderer-test.pcm`,fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)LsLog.i(this.tag, `open file with path: ${this.targetFile.path}`);} catch (e) {LsLog.e(this.tag, `open file failed! -> ${(e as BusinessError).code}:${(e as BusinessError).message}`);}}audio.createAudioRenderer(option,(error, renderer) => {if (error) {LsLog.e(this.tag, `create audio renderer failed! -> ${error.code}:${error.message}`);} else {LsLog.i(this.tag, 'create audio renderer success');this.audioRenderer = renderer;if (renderer) {if (this.withWrite) {renderer.on('writeData', (buffer) => {this.ringBuffer.outFlow(buffer);if (this.targetFile) {const options: WriteOptions = {offset: this.bufferSize,length: buffer.byteLength,}renderer.setVolume(0.75);fileIo.writeSync(this.targetFile.fd, buffer, options);this.bufferSize += buffer.byteLength;}return audio.AudioDataCallbackResult.VALID;});} else {renderer.on('writeData', (buffer) => {this.ringBuffer.outFlow(buffer);return audio.AudioDataCallbackResult.VALID;});}}}});}/** 获取输入流入口 */get inFlow(): AudioBufferFlow {return this.ringBuffer.inFlow;}/** 开始播放 */start(): void {LsLog.i(this.tag, `do start, current state is [${this.audioRenderer?.state}]`);if (this.audioRenderer !== undefined) {let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];if (stateGroup.indexOf(this.audioRenderer.state.valueOf()) === -1) {// 当且仅当状态为prepared、paused和stopped之一时才能启动渲染。LsLog.e(this.tag, 'start failed');return;}// 开始播放。this.audioRenderer.start((err: BusinessError) => {if (err) {LsLog.e(this.tag, `Renderer start failed. -> [${err.code}]:${err.message}`);} else {LsLog.i(this.tag, 'Renderer start success.');}this.ringBuffer.start();});}}/** 停止播放 */stop(): void {LsLog.i(this.tag, `do stop, current state is [${this.audioRenderer?.state}]`);if (this.audioRenderer !== undefined) {const notRunning = this.audioRenderer.state.valueOf() !== audio.AudioState.STATE_RUNNING;const notPaused = this.audioRenderer.state.valueOf() !== audio.AudioState.STATE_PAUSED;if (notRunning && notPaused) {// 只有渲染器状态为running或paused的时候才可以停止。LsLog.i(this.tag, 'Renderer is not running or paused');return;}// 停止渲染。this.audioRenderer.stop((err: BusinessError) => {if (err) {LsLog.e(this.tag, `Renderer stop failed. -> [${err.code}]:${err.message}`);} else {LsLog.i(this.tag, 'Renderer stop success.');}this.ringBuffer.reset();});}}/** 释放资源 */release(): void {if (this.audioRenderer) {this.audioRenderer.release((err: BusinessError) => {if (err) {LsLog.w(this.tag, `release failed! -> ${err.code}: ${err.message}`);} else {LsLog.i(this.tag, 'release success.')}})this.audioRenderer = undefined;}this.ringBuffer.reset();if (this.targetFile) {fileIo.close(this.targetFile.fd);this.targetFile = undefined;}}
}export class AudioCaptureUtil {private readonly tag = "AudioCaptureUtil";private audioCapturer?: audio.AudioCapturer;private waitStartTask?: () => void;private readonly withWrite = false;private targetFile?: fileIo.File;private bufferSize = 0;constructor(context: Context, options: audio.AudioCapturerOptions, flow: AudioBufferFlow) {let permissions: Array<Permissions> = ['ohos.permission.MICROPHONE'];let atManager = abilityAccessCtrl.createAtManager();try {atManager.requestPermissionsFromUser(context, permissions, async (err: BusinessError, data: PermissionRequestResult) => {if (err) {LsLog.e(this.tag, `Request permission failed: ${err.message}`);} else if (data.authResults.includes(-1) || data.authResults.includes(2)) {LsLog.e(this.tag, 'User denied permission');} else {// 用户已授权,再调用 createAudioCapturerthis.prepare(options, flow);}});} catch (err) {LsLog.e(this.tag, `Request permission error: ${err.message}`);}if (this.withWrite) {try {this.targetFile = fileIo.openSync(context.cacheDir + `/capturer-test.pcm`,fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)LsLog.i(this.tag, `open file with path: ${this.targetFile.path}`);} catch (e) {LsLog.e(this.tag, `open file failed! -> ${(e as BusinessError).code}:${(e as BusinessError).message}`);}}}private prepare(options: audio.AudioCapturerOptions, flow: AudioBufferFlow) {LsLog.i(this.tag, `create by ${JSON.stringify(options)}`);this.bufferSize = 0;audio.createAudioCapturer(options,(error, capture) => {if (error) {LsLog.e(this.tag, `create audio capture failed! -> ${error.code}:${error.message}`);} else {LsLog.i(this.tag, 'create audio capture success');this.audioCapturer = capture;if (capture) {if (this.withWrite) {capture.on('readData', (buffer) => {if (this.targetFile) {const options: WriteOptions = {offset: this.bufferSize,length: buffer.byteLength,}fileIo.writeSync(this.targetFile.fd, buffer, options);this.bufferSize += buffer.byteLength;}flow(buffer);});} else {capture.on('readData', flow);}if (this.waitStartTask) {this.start(this.waitStartTask);}}}})}/** 开始录制 */start(onStart: () => void): void {LsLog.i(this.tag, `do start, current state is [${this.audioCapturer?.state}]`);if (this.audioCapturer !== undefined) {this.waitStartTask = undefined;let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];if (stateGroup.indexOf(this.audioCapturer.state.valueOf()) === -1) {// 当且仅当状态为STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一时才能启动采集。LsLog.e(this.tag, 'start failed');return;}// 启动采集。this.audioCapturer.start((err: BusinessError) => {if (err) {LsLog.e(this.tag, `Capturer start failed. -> [${err.code}]:${err.message}`);} else {LsLog.i(this.tag, 'Capturer start success.');onStart();}});} else {this.waitStartTask = onStart;}}/** 停止录制 */stop(): void {LsLog.i(this.tag, `do stop, current state is [${this.audioCapturer?.state}]`);this.waitStartTask = undefined;if (this.audioCapturer !== undefined) {// 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止。const notRunning = this.audioCapturer.state.valueOf() !== audio.AudioState.STATE_RUNNING;const notPaused = this.audioCapturer.state.valueOf() !== audio.AudioState.STATE_PAUSED;if (notRunning && notPaused) {LsLog.i(this.tag, 'Capturer is not running or paused');return;}//停止采集。this.audioCapturer.stop((err: BusinessError) => {if (err) {LsLog.e(this.tag, `Capturer stop failed. -> [${err.code}]:${err.message}`);} else {LsLog.i(this.tag, 'Capturer stop success.');}});}}/** 释放资源 */release(): void {if (this.audioCapturer) {this.audioCapturer.release((err: BusinessError) => {if (err) {LsLog.w(this.tag, `release failed! -> ${err.code}: ${err.message}`);} else {LsLog.i(this.tag, 'release success.')}})this.audioCapturer = undefined;}this.waitStartTask = undefined;if (this.targetFile) {fileIo.close(this.targetFile.fd);this.targetFile = undefined;}}
}
import { audio } from '@kit.AudioKit';const tag = "AudioRingBuffer";/** 音频buffer传递流 */
export type AudioBufferFlow = (buffer: ArrayBuffer) => void;/** 向 buffer 视图写入 */
type DataViewCopy = (from: DataView, to: DataView, fromOffset: number, toOffset: number) => void;/** 运行状态 */
enum RunningState {/** 已停止 */Stop = 0,/** 等待 buffer */WaitingBuffer = 1,/** 正在运行 */Running = 2,
}enum StateIndex {RunningState = 0,ReadPos = 1,WritePos = 2,
}/** 音频 buffer 环形缓冲器 */
export class AudioRingBuffer {/** 缓冲区存储 */private buffer: SharedArrayBuffer;/** 缓冲区视图(用于实际读写操作) */private bufferView: DataView;/** dataViewCopy 数据移动 */private dataViewCopy: DataViewCopy;/** 实际 DataView 可访问的范围 */private readonly bufferSize: number;/** 状态、读写位置指针 */private state = new Int32Array([RunningState.Stop, 0, 1]);/** 音频输入流:将外部数据写入环形缓冲区 */readonly inFlow: AudioBufferFlow = (inBuffer) => {this.workInRunning(() => this.writeToBuffer(inBuffer));};/** 音频输出流:从环形缓冲区读取数据到外部 */readonly outFlow: AudioBufferFlow = (outBuffer) => {this.workInRunning(() => this.readFromBuffer(outBuffer));}/** 获取 DataView 视图的长度 */private dataViewLen: (dataView: DataView) => number;/** Buffer 发声 threshold,buffer 到了此比例才会发声 */private readonly readThreshold: number;/*** 构造音频环形缓冲区* @param streamInfo 音频格式* @param bufferDuration 缓冲时长(秒),建议0.1-1.0之间* @param readThreshold 首帧读取阈值,增加这个值会增加延迟,降低有可能首帧断音*/constructor(streamInfo: audio.AudioStreamInfo, bufferDuration: number = 0.5, readThreshold: number = 0.5) {if (bufferDuration <= 0 || bufferDuration > 1) {const def = 0.5;LsLog.w(tag, `unavalibale bufferDuration: ${bufferDuration}, use default => ${def}`);bufferDuration = def;}if (readThreshold <= 0 || readThreshold > 1) {const def = 0.5;LsLog.w(tag, `unavalibale readThreshold: ${readThreshold}, use default => ${def}`);readThreshold = def;}this.readThreshold = readThreshold;// 计算缓冲区大小:根据音频参数动态计算// 每秒音频数据量const bytesPerSample = this.calcBytesPerSample(streamInfo.sampleFormat);const bytesPerSecond = streamInfo.samplingRate * streamInfo.channels * bytesPerSample;let bufferSize = Math.ceil(bytesPerSecond * bufferDuration); // 缓冲时长对应的字节数// 确保缓冲区大小至少为1024字节,避免过小导致频繁溢出bufferSize = Math.max(bufferSize, 1024);// 初始化缓冲区this.buffer = new SharedArrayBuffer(bufferSize);this.bufferView = new DataView(this.buffer);this.dataViewLen = (view) => Math.ceil(view.byteLength / bytesPerSample);this.bufferSize = this.dataViewLen(this.bufferView);// 初始化读取器、写入器、视图生成器this.dataViewCopy = this.generateDataViewCopy(streamInfo.sampleFormat);LsLog.i(tag,`audio buffer init with ${bufferSize} bytes, duration: ${bufferDuration}s`);}/** 生成 buffer copy */private generateDataViewCopy(format: audio.AudioSampleFormat): DataViewCopy {switch (format) {case audio.AudioSampleFormat.SAMPLE_FORMAT_U8:return (from, to, fromOffset, toOffset) => to.setUint8(toOffset, from.getUint8(fromOffset));case audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE:return (from, to, fromOffset, toOffset) => to.setInt16(toOffset * 2, from.getInt16(fromOffset * 2, true), true);case audio.AudioSampleFormat.SAMPLE_FORMAT_S24LE:return (from, to, fromOffset, toOffset) => {const rawValue = from.getUint8(fromOffset * 4) |(from.getUint8(fromOffset * 4 + 1) << 8) |(from.getUint8(fromOffset * 4 + 2) << 16);// 处理符号扩展const sign = rawValue & 0x800000 ? -1 : 1;const adjustedValue = sign * (rawValue & 0x7FFFFF);to.setInt32(toOffset * 4, adjustedValue, true);}case audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE:return (from, to, fromOffset, toOffset) => to.setInt32(toOffset * 4, from.getInt32(fromOffset * 4, true), true);default:return (from, to, fromOffset, toOffset) => to.setUint8(toOffset, from.getUint8(fromOffset));}}/** 计算每个采样点的数据量 */private calcBytesPerSample(format: audio.AudioSampleFormat): number {switch (format) {case audio.AudioSampleFormat.SAMPLE_FORMAT_U8:return 1;case audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE:return 2;case audio.AudioSampleFormat.SAMPLE_FORMAT_S24LE:return 4;case audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE:return 4;default:return 1;}}/*** 在运行状态下执行任务* @param task 要执行的任务函数*/private workInRunning(task: () => void) {try {if (Atomics.load(this.state, 0) !== RunningState.Stop) {task();}} catch (err) {LsLog.e(tag, `任务执行错误: ${err}`);}}/*** 计算当前可用空间大小* 实际可用空间 = 总容量 - 已使用空间 - 1(预留判断位)*/private getAvailableSpace(): number {return this.bufferSize - 1 - this.getUsedSpace();}/*** 计算当前已使用空间大小*/private getUsedSpace(): number {return (this.getState(StateIndex.WritePos) - this.getState(StateIndex.ReadPos) + this.bufferSize) % this.bufferSize;}/*** 将数据写入环形缓冲区* @param inBuffer 输入数据缓冲区*/private writeToBuffer(inBuffer: ArrayBuffer): void {const inputData = new DataView(inBuffer);const inputLength = this.dataViewLen(inputData);if (inputLength <= 0) {return;}// 获取可用空间并计算实际可写入长度const availableSpace = this.getAvailableSpace();if (inputLength > availableSpace) {LsLog.w(tag,`buffer fulled! has use ${this.getUsedSpace()}, available: ${availableSpace}`);return;}// 处理写入(分是否需要环绕两种情况)const writePos = this.getState(StateIndex.WritePos);const contiguousSpace = this.bufferSize - writePos;if (inputLength <= contiguousSpace) {// 无需环绕,直接写入for (let i = 0; i < inputLength; i++) {this.dataViewCopy(inputData, this.bufferView, i, writePos + i);}this.setState(StateIndex.WritePos, (writePos + inputLength) % this.bufferSize);} else {// 需要环绕,分两部分写入for (let i = 0; i < contiguousSpace; i++) {this.dataViewCopy(inputData, this.bufferView, i, writePos + i);}const remaining = inputLength - contiguousSpace;for (let i = 0; i < remaining; i++) {this.dataViewCopy(inputData, this.bufferView, contiguousSpace + i, i);}this.setState(StateIndex.WritePos, remaining);}}/*** 从环形缓冲区读取数据* @param outBuffer 输出数据缓冲区*/private readFromBuffer(outBuffer: ArrayBuffer): void {const outputData = new DataView(outBuffer);const outputLength = this.dataViewLen(outputData);if (outputLength <= 0) {return;}// 计算可读取数据量const usedSpace = this.getUsedSpace();if (this.getState(StateIndex.RunningState) === RunningState.WaitingBuffer) {if (usedSpace / this.bufferSize < this.readThreshold) {for (let i = 0; i < outputLength; i++) {outputData.setInt8(i, 0);}return;}}this.setState(StateIndex.RunningState, RunningState.Running);const readLength = Math.min(outputLength, usedSpace);// 处理读取(分是否需要环绕两种情况)const readPos = this.getState(StateIndex.ReadPos);const contiguousData = this.bufferSize - readPos;if (readLength <= contiguousData) {for (let i = 0; i < readLength; i++) {this.dataViewCopy(this.bufferView, outputData, readPos + i, i);}this.setState(StateIndex.ReadPos, (readPos + readLength) % this.bufferSize);} else {for (let i = 0; i < contiguousData; i++) {this.dataViewCopy(this.bufferView, outputData, readPos + i, i);}const remaining = readLength - contiguousData;for (let i = 0; i < remaining; i++) {this.dataViewCopy(this.bufferView, outputData, i, contiguousData + i);}this.setState(StateIndex.ReadPos, remaining);}if (readLength < outputLength) {LsLog.w(tag, `read ${outputLength}, but real avalible just ${readLength}, others fill with 0`);for (let i = readLength; i < outputLength; i++) {outputData.setInt8(i, 0);}}}private getState(index: StateIndex): number {return Atomics.load(this.state, index);}private setState(index: StateIndex, value: number) {Atomics.store(this.state, index, value);}/*** 开始流传输*/start() {this.setState(StateIndex.RunningState, RunningState.WaitingBuffer);LsLog.i(tag, "buffer start running");}/*** 重置流(清空缓冲区并重置指针)*/reset() {this.setState(StateIndex.RunningState, RunningState.Stop);this.setState(StateIndex.ReadPos, 0);this.setState(StateIndex.WritePos, 1);LsLog.i(tag, "buffer has reset");}
}

调用

1. 初始化
render = new AudioRenderUtil(context, streamInfo, render.renderInfo);
recordFlow = this.render.inFlow;
capture = new AudioCaptureUtil(context, {streamInfo: streamInfo,capturerInfo: capture.captureInfo}, recordFlow);
2. 开始
/** 开始 capture/render */
private _startKaraoke() {this.capture?.start(() => {// 在录音成功启动后,才有必要开始播放this.render?.start();});
}
3. 停止
/** 停止 capture/render */
private _stopKaraoke() {this.capture?.stop();this.render?.stop();
}
4. 释放
onRelease(): void {this._stopKaraoke();this.capture?.release();this.capture = undefined;this.render?.release();this.render = undefined;
}

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

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

相关文章

洛谷 P1601 A+B Problem(高精)普及-

题目描述 高精度加法&#xff0c;相当于 ab problem&#xff0c;不用考虑负数。 输入格式 分两行输入。a,b≤10500a,b \leq 10^{500}a,b≤10500。 输出格式 输出只有一行&#xff0c;代表 ababab 的值。 输入输出样例 #1 输入 #1 1 1输出 #1 2输入输出样例 #2 输入 #2 1001 909…

Matrix Theory study notes[6]

文章目录linear spacereferenceslinear space a basis of linear space VkV^kVk,which is x1,x2,...xkx_1,x_2,...x_kx1​,x2​,...xk​,can be called as a coordinate system.let vector v∈Vkv \in V^kv∈Vk and it can be linear expressed on this basis as va1x1a2x2...…

专线与专线之间的区别

下面我们从定义、技术特点、适用场景、优缺点等多个维度来详细对比&#xff1a;✅ 一、四种方案简要定义技术方案定义MPLS 专线运营商基于 MPLS 技术提供的私有虚拟网络&#xff0c;逻辑隔离、安全可靠VPN over Internet利用公网加密通道&#xff08;如IPSec&#xff09;构建虚…

Git工作流:团队协作的最佳实践

目录 一、什么是 Git 工作流&#xff1f;为什么需要它&#xff1f; 二、基础&#xff1a;Git 分支核心概念 三、主流 Git 工作流实战指南 1. 集中式工作流&#xff08;Centralized Workflow&#xff09;&#xff1a;适合小团队 / 新手 操作步骤&#xff1a; 优缺点&#…

算法竞赛阶段二-数据结构(35)数据结构单链表模拟实现

//链表--链式存储的线性表 //存信息和下一个节点位置&#xff0c;数据域和指针域合起来叫节点 //带头&#xff08;哨兵位&#xff09;下标为0 //单向&#xff0c;双向&#xff0c;循环链表 //实现 单 //俩足够大数组 // elem&#xff0c;数据域 // next &#xff0c;指针域…

《Computational principles and challenges in single-cell data integration》

1. 引言&#xff1a;单细胞数据整合的背景与重要性单细胞基因组学技术&#xff08;如scRNA-seq、scATAC-seq等&#xff09;近年来快速发展&#xff0c;能够以单细胞分辨率揭示细胞异质性和分子机制。然而&#xff0c;不同实验、样本和数据模态&#xff08;如RNA表达、DNA甲基化…

蔚来汽车携手通义灵码入选 2025 世界人工智能大会标杆案例

7月28日&#xff0c;在2025年世界人工智能大会上&#xff0c;通义灵码助力蔚来汽车研发效能升级成功入选2025年“人工智能”行业标杆案例荟萃。蔚来汽车已有近 1000 名工程师常态化使用通义灵码&#xff0c;AI 生成代码占比超 30%&#xff0c;尤其在蔚来“天探”AI自检系统的建…

Spring Boot中的this::语法糖详解

文章目录前言什么是方法引用&#xff08;Method Reference&#xff09;基本语法方法引用的四种类型1. 静态方法引用2. 实例方法引用&#xff08;特定对象&#xff09;3. 实例方法引用&#xff08;任意对象&#xff09;4. 构造器引用this::在Spring Boot中的应用场景1. Service层…

VitePress学习笔记

VitePress学习笔记VitePress学习搭建和运行编写内容mdvue配置站点配置配置searchsearch 提示词替换使用第三方主题自定义主题设置文档根目录国际化文档navsidebarsearch其他插件vitepress插件markdown-it插件项目开发原始需求和方案自动化流程权限限制VitePress学习 搭建和运行…

C#_创建自己的MyList列表

定义一个数据自己的列表MyList 使用上述描述列表的方式(数组) 列表内也要定义属于自己的方法 例如 Sort排序 Add添加 等等....思路┌─────────────────────────────────────────────────────────────────…

记录Linux下ping外网失败的问题

最近在RK3568上进行开发测试&#xff0c;需要测试一下网络环境&#xff0c;能否通过浏览器访问外部网络。测试情况如下&#xff1a; 1、ping内网、网关ip能ping通 2、ping外网ping不通 情况分析&#xff1a; 1、ping外网失败&#xff08;ping 8.8.8.8也ping不通&#xff0c;说…

Redis 键值对操作详解:Python 实现指南

一、环境准备 1. 安装依赖库 pip install redis2. 连接 Redis 数据库 import redis# 创建 Redis 客户端连接 r redis.Redis(hostlocalhost, # Redis 服务器地址port6379, # Redis 端口db0, # 数据库编号&#xff08;0~15&#xff09;passwordNone, …

制造业企业大文件传输的痛点有哪些?

在全球化与数字化的浪潮下&#xff0c;制造业企业的大文件传输需求日益凸显&#xff0c;然而诸多痛点也随之而来&#xff0c;严重制约着企业的高效运营与发展。复杂网络环境导致传输稳定性差制造业企业常涉及跨地域、跨国的业务合作与数据交流&#xff0c;网络环境复杂多变。在…

低速信号设计之 MDIO 篇

一、引言​ 在服务器的网络子系统中,MDIO(Management Data Input/Output)总线虽然传输速率相对较低,却扮演着极为关键的角色。它主要负责在 MAC(Media Access Control)层器件与 PHY(Physical Layer)层器件之间搭建起通信的桥梁,实现对 PHY 层器件的有效管理与状态监控…

AR技术赋能航空维修:精度与效率的飞跃

在航空工业领域&#xff0c;飞机维修与装配的精度要求越来越高。传统的维修方法依赖人工操作和经验判断&#xff0c;容易产生误差。随着增强现实&#xff08;AR www.teamhelper.cn &#xff09;技术的引入&#xff0c;航空维修迎来了革命性的变化。本文将探讨AR技术在航空维修中…

设计模式实战:自定义SpringIOC(理论分析)

自定义SpringIOC&#xff08;理论分析&#xff09; 上一篇&#xff1a;设计模式开源实战&#xff1a;观察者模式不知道怎么用&#xff1f;手撕Spring源码中跟着大佬学编程 上一篇我们研究了大佬在Spring源码中使用的观察者模式&#xff0c;今天我们再来聊聊Spring的核心功能—…

人工智能如何改变项目管理:应用、影响与趋势

人工智能如何改变项目管理&#xff1a;应用、影响与趋势1. 人工智能如何提升项目规划与进度安排2. 人工智能在资源分配与优化中的应用3. 人工智能用于风险管理4. 人工智能用于团队协作与交流5. 人工智能用于项目监控与报告6. 集成人工智能的项目管理软件6.1 Wrike6.2 ClickUp6.…

【MySql】事务的原理

​ 【MySql】事务的原理数据库的隔离级别原理读未提交读已提交可重复读&#xff08;Repeatable Read&#xff09;串行化&#xff08;最高的隔离级别&#xff0c;强制事务串行执行&#xff0c;避免了所有并发问题&#xff09;MVCC&#xff08;Multi-Version Concurrency Control…

YOLO--目标检测基础

一、基本认知1.1目标检测的定义目标检测&#xff08;Object Detection&#xff09;&#xff1a;在图像或视频中检测出目标图像的位置&#xff0c;并进行分类和识别的相关任务。主要是解决图像是什么&#xff0c;在哪里的两个具体问题。1.2使用场景目标检测的使用场景众多&#…

GitLab 18.2 发布几十项与 DevSecOps 有关的功能,可升级体验【四】

沿袭我们的月度发布传统&#xff0c;极狐GitLab 发布了 18.2 版本&#xff0c;该版本带来了议题和任务的自定义工作流状态、新的合并请求主页、新的群组概览合规仪表盘、下载安全报告的 PDF 导出文件、中心化的安全策略管理&#xff08;Beta&#xff09;等几十个重点功能的改进…