純血鴻蒙 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
英文地址,請注明出處:http://en.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;等幾十個重點功能的改進…