当前位置: 首页 > news >正文

纯血鸿蒙 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.dtcms.com/a/305312.html

相关文章:

  • 选用Java开发商城的优势
  • 功率场效应晶体管MOSFET关键指标
  • 岩石图文分析系统
  • Gartner发布2025年数据安全技术成熟度曲线:29项最新数据安全相关技术发展和应用趋势
  • 【SQL】Windows MySQL 服务查询启动停止自启动(保姆级)
  • 学习日志21 python
  • Sub-GHz射频技术,智能安防系统的“长续航、深覆盖”密码
  • 集成开发环境(IDE)
  • 卸油作业安全设施识别准确率↑32%:陌讯多模态融合算法实战解析
  • Layui表格备注编辑功能代码详解
  • NAT:网络地址转换
  • 开发避坑短篇(8):Java Cookie值非法字符异常分析与解决方案:IllegalArgumentException[32]
  • 前端css 的固定布局,流式布局,弹性布局,自适应布局,响应式布局
  • redis得到shell的几种方法
  • Python包架构设计与模式应用:构建可扩展的企业级组件
  • 本土化DevOps实践:Gitee为核心的协作工具链与高效落地指南
  • Java 11 新特性详解与代码示例
  • 《C++》STL--vector容器超详细解析
  • CSS 在单页应用(SPA)中的适用性解析与实践
  • QWebEngineProfile setCachePath无效
  • aar, aab,apk三种应用格式的区别
  • Linux网络编程——IP地址与端口、通信协议、Socket套接字基础概念解析
  • 【C语言】指针深度剖析(一)
  • Router 动态路由
  • FPGA数码管驱动模块
  • Netty中FastThreadLocal解读
  • C++多态:面向对象编程的灵魂之
  • Linux_库制作与原理浅理解
  • 青木川古镇
  • Flex布局面试常考的场景题目