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

📅 2026/7/20 12:27:05
HarmonyOS 6.0 录音开发——从采集到文件
录音在HarmonyOS里分两层底层的AudioCapturer直接操作PCM数据上层的media.createSoundPool做短音效。语音消息、会议记录用AudioCapturer按钮点击音效用SoundPool。这篇把两条线都走通。权限录音需要MICROPHONE权限// module.json5 { name: ohos.permission.MICROPHONE }动态申请不能省import{abilityAccessCtrl,common}fromkit.AbilityKit;asyncrequestMic(context:common.UIAbilityContext):Promiseboolean{letatManager:abilityAccessCtrl.AccessManagerabilityAccessCtrl.createAtManager();letresultawaitatManager.requestPermissionsFromUser(context,[ohos.permission.MICROPHONE]);returnresult.authResults[0]0;}没授权就调录音API直接报201权限错误。AudioCapturer基础import{audio}fromkit.AudioKit;letaudioCapturer:audio.AudioCapturerawaitaudio.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够语音识别用channels1单声道2立体声语音消息用单声道就行sampleFormatS16LE表示16位小端最通用的格式encodingTypeRAW表示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:numberawaitaudioCapturer.getBufferSize();letbuffer:ArrayBuffernewArrayBuffer(bufferSize);asyncfunctionreadLoop():Promisevoid{while(audioCapturer.stateaudio.AudioState.STATE_RUNNING){letreadSize:numberawaitaudioCapturer.read(buffer);if(readSize0){// 处理PCM数据: buffer的前readSize字节processPcmData(buffer,readSize);}}}read()是异步的返回实际读取的字节数。readSize可能小于bufferSize——这是正常的尤其在录音刚开始时。读取循环通常跑在单独的任务里避免阻塞UI。HarmonyOS推荐用TaskPoolimport{taskpool}fromkit.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:Int16ArraynewInt16Array(buffer);letsum:number0;letcount:numberMath.floor(readSize/2);for(leti0;icount;i){letsample:numberview[i];sumsample*sample;}letrms:numberMath.sqrt(sum/count);returnrms/32768;// 归一化到0-1}Int16Array对应S16LE格式。32768是16位最大值归一化后RMS在0-1范围。保存为WAV文件PCM数据保存为WAV需要加文件头import{fileIoasfs}fromkit.CoreFileKit;functionwriteWavHeader(fd:number,sampleRate:number,channels:number,bitsPerSample:number,dataLength:number):void{letheaderSize:number44;letheader:ArrayBuffernewArrayBuffer(headerSize);letview:DataViewnewDataView(header);// RIFF chunkview.setUint8(0,0x52);view.setUint8(1,0x49);view.setUint8(2,0x46);view.setUint8(3,0x46);view.setUint32(4,36dataLength,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:numberfs.openSync(filePath,fs.OpenMode.CREATE|fs.OpenMode.READ_WRITE);writeWavHeader(fd,44100,1,16,0);// dataLength先写0// 录音过程中追加数据lettotalDataLength:number0;audioCapturer.on(readData,(buffer:ArrayBuffer){fs.writeSync(fd,buffer);totalDataLengthbuffer.byteLength;});// 停止录音后fs.lseekSync(fd,4,0);// 偏移到RIFF size字段letview:DataViewnewDataView(newArrayBuffer(4));view.setUint32(0,36totalDataLength,true);fs.writeSync(fd,view.buffer);// 更新RIFF sizefs.lseekSync(fd,40,0);// 偏移到data size字段letview2:DataViewnewDataView(newArrayBuffer(4));view2.setUint32(0,totalDataLength,true);fs.writeSync(fd,view2.buffer);// 更新data sizefs.closeSync(fd);录音时长计算letrecordedDuration:number0;// 毫秒audioCapturer.on(readData,(buffer:ArrayBuffer){letbytesPerSample:number2;// S16LE 2 bytesletchannels:number1;letsampleRate:number44100;letbytesPerMs:numbersampleRate*channels*bytesPerSample/1000;recordedDurationbuffer.byteLength/bytesPerMs;});每收到一段PCM数据根据字节长度和采样参数计算对应时长。累加就是总录音时长。最大录音时长限制letmaxDurationMs:number60000;// 60秒audioCapturer.on(readData,(buffer:ArrayBuffer){this.recordedDurationbuffer.byteLength/bytesPerMs;if(this.recordedDurationmaxDurationMs){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():Promisevoid{if(this.audioCapturer){if(this.audioCapturer.stateaudio.AudioState.STATE_RUNNING){awaitthis.audioCapturer.stop();}awaitthis.audioCapturer.release();this.audioCapturerundefined;}}在aboutToDisappear中调用releaseCapturer。不释放的话其他应用无法录音。SoundPool——短音效播放录音的对立面是播放。SoundPool专门用于短音效加载快、延迟低import{media}fromkit.MultimediaKit;letsoundPool:media.SoundPoolawaitmedia.createSoundPool(5,audio.AudioRendererInfo());letsoundId:numberawaitsoundPool.load(/data/storage/el2/base/haps/entry/files/click.wav);letstreamId:numberawaitsoundPool.play(soundId);createSoundPool的第一个参数是最大并发流数5表示最多同时播放5个音效。load返回soundIdplay返回streamId。// 停止特定音效soundPool.stop(streamId);// 设置音量soundPool.setVolume(streamId,1.0,1.0);// 左右声道// 释放soundPool.release();SoundPool适合按钮点击、游戏音效等场景。长音频用AVPlayer。音频焦点录音时如果其他应用在播放音频需要请求音频焦点letaudioManager:audio.AudioManageraudio.getAudioManager();letaudioSessionManager:audio.AudioSessionManageraudioManager.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再readWAV文件播放不了文件头dataLength不对录完后seek更新头录音时长不准bytesPerMs计算错误检查sampleRate和channels其他应用无法录音没releaseaboutToDisappear释放SoundPool load失败文件路径不对检查文件是否存在录音有噪音采样率太低或增益过高44100采样率适当增益录音开发最关键的三个点权限必须在录音前申请、状态机必须按顺序推进、PCM数据转WAV时文件头要正确。这三点搞对了基本不会出大问题。