news 2026/7/20 12:27:02

HarmonyOS 6.0 录音开发——从采集到文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS 6.0 录音开发——从采集到文件

录音在HarmonyOS里分两层:底层的AudioCapturer直接操作PCM数据,上层的media.createSoundPool做短音效。语音消息、会议记录用AudioCapturer,按钮点击音效用SoundPool。这篇把两条线都走通。

权限

录音需要MICROPHONE权限:

// module.json5 { "name": "ohos.permission.MICROPHONE" }

动态申请不能省:

import{abilityAccessCtrl,common}from'@kit.AbilityKit';asyncrequestMic(context:common.UIAbilityContext):Promise<boolean>{letatManager:abilityAccessCtrl.AccessManager=abilityAccessCtrl.createAtManager();letresult=awaitatManager.requestPermissionsFromUser(context,['ohos.permission.MICROPHONE']);returnresult.authResults[0]===0;}

没授权就调录音API直接报201权限错误。

AudioCapturer基础

import{audio}from'@kit.AudioKit';letaudioCapturer:audio.AudioCapturer=awaitaudio.createAudioCapturer({samplingRate:audio.AudioSamplingRate.SAMPLE_RATE_44100,channels:audio.AudioChannel.CHANNEL_1,sampleFormat:audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,encodingType:audio.AudioEncodingType.ENCODING_TYPE_RAW});

四个参数决定PCM数据的格式:

  • samplingRate:采样率,44100是CD品质,16000够语音识别用
  • channels:1单声道2立体声,语音消息用单声道就行
  • sampleFormat:S16LE表示16位小端,最通用的格式
  • encodingType:RAW表示PCM裸数据

录音状态机

AudioCapturer有五个状态:

CREATED → PREPARED → RUNNING → PAUSED → STOPPED

每次状态转换都有对应方法:

awaitaudioCapturer.prepare();// CREATED → PREPAREDawaitaudioCapturer.start();// PREPARED → RUNNINGawaitaudioCapturer.pause();// RUNNING → PAUSEDawaitaudioCapturer.stop();// RUNNING/PAUSED → STOPPEDawaitaudioCapturer.release();// 任何状态 → RELEASED

从PAUSED恢复用start()不是resume()。从STOPPED回到RUNNING要重新prepare + start。

读取PCM数据

letbufferSize:number=awaitaudioCapturer.getBufferSize();letbuffer:ArrayBuffer=newArrayBuffer(bufferSize);asyncfunctionreadLoop():Promise<void>{while(audioCapturer.state===audio.AudioState.STATE_RUNNING){letreadSize:number=awaitaudioCapturer.read(buffer);if(readSize>0){// 处理PCM数据: buffer的前readSize字节processPcmData(buffer,readSize);}}}

read()是异步的,返回实际读取的字节数。readSize可能小于bufferSize——这是正常的,尤其在录音刚开始时。

读取循环通常跑在单独的任务里,避免阻塞UI。HarmonyOS推荐用TaskPool:

import{taskpool}from'@kit.ArkTS';@ConcurrentfunctioncaptureTask(capturer:audio.AudioCapturer):void{// 读取循环}taskpool.execute(captureTask,audioCapturer);

但AudioCapturer是Native对象,不能直接跨线程传递。实际操作中更常见的方式是用setInterval定时读取。

录音回调方式

除了主动read,还可以用回调方式获取数据:

audioCapturer.on('readData',(buffer:ArrayBuffer)=>{// buffer就是PCM数据processPcmData(buffer,buffer.byteLength);});

回调方式更简洁,不用自己管读取循环。但回调频率很高(每秒几十次),处理函数要尽量轻。

音量监听

audioCapturer.on('markReach',1000,(position:number)=>{// 每采集1000帧触发一次});// 实时音量(用于波形显示)audioCapturer.on('periodReach',200,(position:number)=>{// 每200帧触发});

要获取实时音量做波形显示,需要在readData回调里计算PCM数据的RMS值:

functioncalculateRms(buffer:ArrayBuffer,readSize:number):number{letview:Int16Array=newInt16Array(buffer);letsum:number=0;letcount:number=Math.floor(readSize/2);for(leti=0;i<count;i++){letsample:number=view[i];sum+=sample*sample;}letrms:number=Math.sqrt(sum/count);returnrms/32768;// 归一化到0-1}

Int16Array对应S16LE格式。32768是16位最大值,归一化后RMS在0-1范围。

保存为WAV文件

PCM数据保存为WAV需要加文件头:

import{fileIoasfs}from'@kit.CoreFileKit';functionwriteWavHeader(fd:number,sampleRate:number,channels:number,bitsPerSample:number,dataLength:number):void{letheaderSize:number=44;letheader:ArrayBuffer=newArrayBuffer(headerSize);letview:DataView=newDataView(header);// RIFF chunkview.setUint8(0,0x52);view.setUint8(1,0x49);view.setUint8(2,0x46);view.setUint8(3,0x46);view.setUint32(4,36+dataLength,true);view.setUint8(8,0x57);view.setUint8(9,0x41);view.setUint8(10,0x56);view.setUint8(11,0x45);// fmt chunkview.setUint8(12,0x66);view.setUint8(13,0x6D);view.setUint8(14,0x74);view.setUint8(15,0x20);view.setUint32(16,16,true);// chunk sizeview.setUint16(20,1,true);// PCM formatview.setUint16(22,channels,true);view.setUint32(24,sampleRate,true);view.setUint32(28,sampleRate*channels*bitsPerSample/8,true);// byte rateview.setUint16(32,channels*bitsPerSample/8,true);// block alignview.setUint16(34,bitsPerSample,true);// data chunkview.setUint8(36,0x64);view.setUint8(37,0x61);view.setUint8(38,0x74);view.setUint8(39,0x61);view.setUint32(40,dataLength,true);fs.writeSync(fd,header);}

WAV文件头固定44字节。先写头再写PCM数据。但有个问题:录音开始时不知道总数据长度,所以通常先写44字节的占位头,录完后seek回文件头更新dataLength。

// 开始录音时letfd:number=fs.openSync(filePath,fs.OpenMode.CREATE|fs.OpenMode.READ_WRITE);writeWavHeader(fd,44100,1,16,0);// dataLength先写0// 录音过程中追加数据lettotalDataLength:number=0;audioCapturer.on('readData',(buffer:ArrayBuffer)=>{fs.writeSync(fd,buffer);totalDataLength+=buffer.byteLength;});// 停止录音后fs.lseekSync(fd,4,0);// 偏移到RIFF size字段letview:DataView=newDataView(newArrayBuffer(4));view.setUint32(0,36+totalDataLength,true);fs.writeSync(fd,view.buffer);// 更新RIFF sizefs.lseekSync(fd,40,0);// 偏移到data size字段letview2:DataView=newDataView(newArrayBuffer(4));view2.setUint32(0,totalDataLength,true);fs.writeSync(fd,view2.buffer);// 更新data sizefs.closeSync(fd);

录音时长计算

letrecordedDuration:number=0;// 毫秒audioCapturer.on('readData',(buffer:ArrayBuffer)=>{letbytesPerSample:number=2;// S16LE = 2 bytesletchannels:number=1;letsampleRate:number=44100;letbytesPerMs:number=sampleRate*channels*bytesPerSample/1000;recordedDuration+=buffer.byteLength/bytesPerMs;});

每收到一段PCM数据,根据字节长度和采样参数计算对应时长。累加就是总录音时长。

最大录音时长限制

letmaxDurationMs:number=60000;// 60秒audioCapturer.on('readData',(buffer:ArrayBuffer)=>{this.recordedDuration+=buffer.byteLength/bytesPerMs;if(this.recordedDuration>=maxDurationMs){audioCapturer.stop();}});

在readData回调里检查累计时长,超过限制自动停止。微信语音消息60秒就是这么实现的。

录音状态监听

audioCapturer.on('stateChange',(state:audio.AudioState)=>{switch(state){caseaudio.AudioState.STATE_PREPARED:// 准备完成break;caseaudio.AudioState.STATE_RUNNING:// 录音中break;caseaudio.AudioState.STATE_PAUSED:// 暂停break;caseaudio.AudioState.STATE_STOPPED:// 已停止break;caseaudio.AudioState.STATE_RELEASED:// 已释放break;}});

UI状态通常根据stateChange回调更新,比如录音按钮的图标切换。

资源释放

录音结束后必须释放资源:

asyncreleaseCapturer():Promise<void>{if(this.audioCapturer){if(this.audioCapturer.state===audio.AudioState.STATE_RUNNING){awaitthis.audioCapturer.stop();}awaitthis.audioCapturer.release();this.audioCapturer=undefined;}}

在aboutToDisappear中调用releaseCapturer。不释放的话其他应用无法录音。

SoundPool——短音效播放

录音的对立面是播放。SoundPool专门用于短音效,加载快、延迟低:

import{media}from'@kit.MultimediaKit';letsoundPool:media.SoundPool=awaitmedia.createSoundPool(5,audio.AudioRendererInfo());letsoundId:number=awaitsoundPool.load('/data/storage/el2/base/haps/entry/files/click.wav');letstreamId:number=awaitsoundPool.play(soundId);

createSoundPool的第一个参数是最大并发流数,5表示最多同时播放5个音效。load返回soundId,play返回streamId。

// 停止特定音效soundPool.stop(streamId);// 设置音量soundPool.setVolume(streamId,1.0,1.0);// 左右声道// 释放soundPool.release();

SoundPool适合按钮点击、游戏音效等场景。长音频用AVPlayer。

音频焦点

录音时如果其他应用在播放音频,需要请求音频焦点:

letaudioManager:audio.AudioManager=audio.getAudioManager();letaudioSessionManager:audio.AudioSessionManager=audioManager.getSessionManager();awaitaudioSessionManager.activate(audio.AudioSessionStrategy.create({concurrency:audio.AudioConcurrency.EXCLUSIVE}));

EXCLUSIVE表示独占——录音时其他音频暂停。CONCURRENCY_MIX表示混音。

踩坑清单

问题原因解决
createAudioCapturer报201缺MICROPHONE权限动态申请
start()报错没先prepare按CREATED→PREPARED→RUNNING顺序
read返回0capturer没在RUNNING状态检查state再read
WAV文件播放不了文件头dataLength不对录完后seek更新头
录音时长不准bytesPerMs计算错误检查sampleRate和channels
其他应用无法录音没releaseaboutToDisappear释放
SoundPool load失败文件路径不对检查文件是否存在
录音有噪音采样率太低或增益过高44100采样率+适当增益

录音开发最关键的三个点:权限必须在录音前申请、状态机必须按顺序推进、PCM数据转WAV时文件头要正确。这三点搞对了,基本不会出大问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/20 12:25:55

GPT-5.6 更聪明之外:模型竞争开始算成本账

GPT-5.6 把模型升级拉回成本、稳定性和拒答边界。 从每 token 更多智能到模糊输入拒答&#xff0c;开发者该重新评估模型真实稳定性。大模型发布会越来越像一场账本公开。 过去大家盯着模型参数、上下文长度和榜单名次。现在真正敏感的问题变成了&#xff1a;同样一美元、同样一…

作者头像 李华
网站建设 2026/7/20 12:25:53

影刀RPA 流程审计日志:谁在什么时候做了什么

影刀RPA 流程审计日志&#xff1a;谁在什么时候做了什么 作者&#xff1a;林焱 什么情况用 流程跑着跑着出了问题&#xff0c;你要排查&#xff0c;发现——不知道哪一步出的错、不知道什么时间跑的、不知道输入是什么。只能从头到尾加断点重跑一遍&#xff0c;整个过程像在抓…

作者头像 李华
网站建设 2026/7/20 12:25:49

C++手把手解析OBJ文件:从格式原理到MeshLab可视化验证

1. 项目概述与核心价值 最近在整理一些三维模型处理的老项目&#xff0c;发现很多朋友对如何从最原始的模型文件格式入手&#xff0c;理解三维数据的底层结构很感兴趣。尤其是OBJ这种“古老”但生命力顽强的格式&#xff0c;它就像三维世界的“TXT”文件&#xff0c;结构清晰&…

作者头像 李华
网站建设 2026/7/20 12:25:33

2023任天堂夏季直面会:28款游戏发售日期与亮点解析

1. 任天堂直面会2023夏季游戏阵容全解析北京时间6月21日晚&#xff0c;任天堂举办了2023年度夏季直面会&#xff08;Nintendo Direct&#xff09;&#xff0c;这场时长40分钟的发布会堪称近年来内容最密集的一次。作为从业15年的游戏媒体人&#xff0c;我完整梳理了本次公布的2…

作者头像 李华
网站建设 2026/7/20 12:24:43

深入解析TI Jacinto VIP_PARSER:多路视频复用、裁剪与中断处理实战

1. VIP_PARSER&#xff1a;汽车信息娱乐系统的视频处理核心在汽车信息娱乐系统、高级驾驶辅助系统&#xff08;ADAS&#xff09;以及多摄像头监控系统中&#xff0c;一个核心挑战是如何高效、可靠地处理来自多个摄像头的视频流。这些摄像头可能遍布车身四周&#xff0c;提供环视…

作者头像 李华