HarmonyOS/OpenHarmony Audio 实现音频录制及播放功能

关键词:audio、音频录制、音频播放、权限申请、文件管理

在app的开发过程中时常会遇见一些需要播放一段音频或进行语音录制的场景,那么本期将介绍如何利用鸿蒙 audio 模块实现音频写入和播放的功能。本次依赖的是 ohos.multimedia.audio 音频管理模块,核心逻辑为利用 AudioCapturer  创建音频采集器收集音频并写入文件至沙箱,利用 AudioRenderer 播放沙箱中写入的音频文件,确定目标那么开始。

本期文章的完整demo代码已经提交至Gitee:https://gitee.com/luvi/sound-recording

1. 添加权限

需要录音,必不可少的是麦克风权限,需要在 module.json5 中添加 ohos.permission.MICROPHONE 权限。

2. 引导用户授权

在第一步添加完麦克风权限后,app开启后并不能直接使用该权限,用户需要手动确认麦克风权限的开启,在用户手动确认后,麦克风权限则开始在当前app生效。

所以,在代码中我们需要进行访问权限控制弹窗的拉起操作,在这里使用 requestPermissionsFromUser 即可。需要注意的是,若用户拒绝权限后,下次需要引导用户前往设置页手动打开该权限,此处就不做过多逻辑处理,默认用户会同意该权限。

// 此处需要导入权限控制模块
import { abilityAccessCtrl, Permissions,PermissionRequestResult } from '@kit.AbilityKit';let permissionList: Permissions[] = ["ohos.permission.MICROPHONE"]
// 获取访问控制模块对象
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
let context: Context = getContext(this) as common.UIAbilityContext;
atManager.requestPermissionsFromUser(context, permissionList, (err: BusinessError, data: PermissionRequestResult) => {if (err) {console.error(`luvi > requestPermissionsFromUser fail, err->${JSON.stringify(err)}`);} else {// 权限获取成功console.info('luvi > data:' + JSON.stringify(data));}
});

3. 创建 AudioCapturer 音频采集器,准备录音

在第2部授权操作完成后才可进行 AudioCapturer 音频采集器的创建,不然没有权限是会报系统异常的错误。

// 此audioCapturer是写在struct中,自行修改位置
audioCapturer: audio.AudioCapturer | null = null...let audioCapturerOptions: audio.AudioCapturerOptions = {// 音频流信息streamInfo: {samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,channels: audio.AudioChannel.CHANNEL_2,sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW},// 采集器信息capturerInfo: {source: audio.SourceType.SOURCE_TYPE_MIC,capturerFlags: 0}
}// 创建音频采集器
audio.createAudioCapturer(audioCapturerOptions, (err, data) => {if (err) {console.error(`luvi > AudioCapturer Created : Error: ${err}`);} else {console.info('luvi > AudioCapturer Created : Success.');// 音频采集器对象this.audioCapturer = data;}
});

4. 开始录音

在第3步的操作后,我们已经拿到了 audioCapturer 对象,后续需要通过该对象进行音频录制与取消。

在录音过程中,需要不断的写入声音数据到文件中,所以我们需要订阅音频数据读入回调事件 后触发 start 操作开始录音,在文件数据写入前需要增加 fs.OpenMode.READ_WRITE 权限。此处需要注意的是 MyVoice.wav 文件本身并不存在与沙箱文件中,但是我们使用文件管理的 open 方法配置 fs.OpenMode.CREATE 权限则会自动创建出该文件。

// 导入文件管理模块
import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';...// struct中
destFile: fs.File | null = null...Button("开始采集语音").onClick(() => {let path = getContext().getApplicationContext().filesDir;let bufferSize: number = 0;let filePath = path + '/MyVoice.wav';this.destFile = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);let readDataCallback = (buffer: ArrayBuffer) => {let options: ReadOptions = {offset: bufferSize,length: buffer.byteLength}fs.writeSync(this.destFile?.fd, buffer, options);bufferSize += buffer.byteLength;}this.audioCapturer?.on('readData', readDataCallback);this.audioCapturer?.start((err: BusinessError) => {if (err) {console.error('luvi > Capturer start failed.');} else {console.info('luvi > Capturer start success.');}});
})

5. 结束录音

录音结束后关闭文件操作,避免资源占用。

Button("结束采集音频").onClick(() => {this.audioCapturer?.stop((err: BusinessError) => {if (err) {console.error('luvi > Capturer stop failed');} else {console.info('luvi > Capturer stopped.');}});fs.close(this.destFile)
})

此时录制的音频已经保存至了沙箱中。

6. 创建音频渲染器

audioRenderer 是写在 struct 中,需要保存音频渲染器对象供后续使用。

// 此audioRenderer是写在struct中,自行修改位置
audioRenderer: audio.AudioRenderer| null = null...
let audioRendererOptions: audio.AudioRendererOptions = {streamInfo: {samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率channels: audio.AudioChannel.CHANNEL_2, // 通道sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式},rendererInfo: {content: audio.ContentType.CONTENT_TYPE_MUSIC, // 媒体类型usage: audio.StreamUsage.STREAM_USAGE_MEDIA, // 音频流使用类型rendererFlags: 0 // 音频渲染器标志}
}audio.createAudioRenderer(audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例if (!err) {console.info(`luvi > creating AudioRenderer success`);// 音频渲染器对象this.audioRenderer = renderer;} else {console.info(`luvi > creating AudioRenderer failed, error: ${err.message}`);}
});

7.播放音频

播放第5步保存的音频文件,需要使用音频渲染器对象,创建的渲染器本身无音频对象,所以需要在启动音频渲染器后,不断地在音频渲染器中写入音频文件的缓冲数据,从而达到播放效果,当播放完毕后关闭文件和渲染器。

Button("播放音频采集结果").onClick(async () => {if (!this.audioRenderer){return}let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];if (stateGroup.indexOf(this.audioRenderer.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染console.error('luvi > start failed');return;}await this.audioRenderer.start(); // 启动渲染const bufferSize = await this.audioRenderer.getBufferSize();let context = getContext(this).getApplicationContext();let path = context.filesDir;const filePath = path + '/MyVoice.wav'; // 使用沙箱路径获取文件,实际路径为/data/storage/el2/base/haps/entry/files/test.wavlet file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);let stat = await fs.stat(filePath);let buf = new ArrayBuffer(bufferSize);let len = stat.size % bufferSize === 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1);for (let i = 0; i < len; i++) {let options: ReadOptions = {offset: i * bufferSize,length: bufferSize};let readsize = await fs.read(file.fd, buf, options);// buf是要写入缓冲区的音频数据,在调用AudioRenderer.write()方法前可以进行音频数据的预处理,实现个性化的音频播放功能,AudioRenderer会读出写入缓冲区的音频数据进行渲染let writeSize: number = await new Promise((resolve, reject) => {this.audioRenderer?.write(buf, (err, writeSize) => {if (err) {reject(err);} else {resolve(writeSize);}});});if (this.audioRenderer.state === audio.AudioState.STATE_RELEASED) { // 如果渲染器状态为released,停止渲染fs.close(file);await this.audioRenderer.stop();}if (this.audioRenderer.state === audio.AudioState.STATE_RUNNING) {if (i === len - 1) { // 如果音频文件已经被读取完,停止渲染fs.close(file);await this.audioRenderer.stop();}}}
})

此时,我们就已经完成了音频录制与播放的一整套功能,若在开发中遇到问题可连接设备点击 IDE 右下角的 Device File Browser 文件浏览器,查看音频文件写入是否正确,还有最重要的就算别忘记添加权限。

完整代码已经提交至了Gitee中,可回顶部查看。

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

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

相关文章

AI日常绘画【国庆海报】:盛世迎华诞,Flux国庆节海报制作教程

大家好我是极可菌&#xff01;&#xff01;&#xff01; 马上就要到祖国母亲的节日了&#xff0c;想想心里都美滋滋的&#xff0c;终于可以放松一下了。相信AI绘画关于国庆主题肯定也会精彩纷呈吧&#xff0c;今天和大家分享几组关于国庆海报的制作教程。 本文使用基于Flux的相…

windows 驱动实例分析系列-定时日志的COM驱动

本文章的前置文章为: windows 驱动编写原则 windows COM驱动 案例 windows COM驱动的I/O处理 在前面的设计中,主要是对windows提供的VirtualSerial源代码的讲解,但是那个驱动其实是一个空壳驱动,用于学习的,在I/O处理中,也讲述了serial I/O处理的本质,接下来会将这些…

【EXCEL数据处理】000009 案列 EXCEL单元格数字格式。文本型数字格式和常规型数字格式的区别

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000009 案列 EXCEL单元格数字格式。文本型数字格式和…

如何使用SCCMSecrets识别SCCM策略中潜在的安全问题

关于SCCMSecrets SCCMSecrets是一款针对SCCM策略的安全扫描与检测工具&#xff0c;该工具旨在提供一种有关 SCCM 策略的全面安全检测方法。 该工具可以从各种权限级别执行&#xff0c;并将尝试发现与策略分发相关的潜在错误配置。除了分发点上托管的包脚本外&#xff0c;它还将…

【C++篇】启航——初识C++(下篇)

接上篇【C篇】启航——初识C&#xff08;上篇&#xff09; 目录 一、引用 1.引用的概念 2.引用的基本语法 3.引用的特点 3.1 别名 3.2 不占用额外内存 3.3 必须初始化 3.4 不能为 NULL 4.引用的使用 4.1 函数参数传递 4.2 返回值 4.3 常量引用 5.引用和指针的关…

网站建设公司如何选?2024专业网站建设公司哪家好TOP3

要找一家靠谱的网站建设公司&#xff0c;可以根据以下五点判断&#xff1a; 1.企业的工商信息 企业有多少人、什么时候成立的、成立资金是多少、是否有违约记录这些都可以在查企业的那种app里可以看到&#xff0c;去查的时候一定要仔细甄别&#xff0c;别最后找了一家皮包公司…

PCL 索引空间采样

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.1.1 索引空间采样 2.1.2 可视化原始点云和下采样后的点云 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接&#xff1a; PCL点云算法与项目实战案例汇总&#xf…

软件设计师——计算机网络

&#x1f4d4;个人主页&#x1f4da;&#xff1a;秋邱-CSDN博客☀️专属专栏✨&#xff1a;软考——软件设计师&#x1f3c5;往期回顾&#x1f3c6;&#xff1a;&#x1f31f;其他专栏&#x1f31f;&#xff1a;C语言_秋邱 一、OSI/ RM七层模型(⭐⭐⭐) ​ 层次 名称 主要功…

【Iceberg分析】调研Iceberg中表的原地演变

调研Iceberg中表的原地演变 文章目录 调研Iceberg中表的原地演变原生非分区表文件关系图表的原地演变之表schema演变新增字段new_column文件关系变化图为新增字段写入数据文件关系变化图删除新增字段文件关系变化图新增字段new_column2文件关系变化图删除数据文件关系变化图 原…

C++ | Leetcode C++题解之第433题最小基因变化

题目&#xff1a; 题解&#xff1a; class Solution { public:int minMutation(string start, string end, vector<string>& bank) {int m start.size();int n bank.size();vector<vector<int>> adj(n);int endIndex -1;for (int i 0; i < n; i)…

爬虫及数据可视化——运用Hadoop和MongoDB数据进行分析

作品详情  运用Hadoop和MongoDB对得分能力数据进行分析&#xff1b;  运用python进行机器学习的模型调理&#xff0c;利用Pytorch框架对爬取的评论进行情感分析预测&#xff1b;  利用python和MySQL对网站的数据进行爬取、数据清洗及可视化。

快速实现AI搜索!Fivetran 支持 Milvus 作为数据迁移目标

Fivetran 现已支持 Milvus 向量数据库作为数据迁移的目标&#xff0c;能够有效简化 RAG 应用和 AI 搜索中数据源接入的流程。 数据是 AI 应用的支柱&#xff0c;无缝连接数据是充分释放数据潜力的关键。非结构化数据对于企业搜索和检索增强生成&#xff08;RAG&#xff09;聊天…

SpringBoot框架下体育馆管理系统的构建

1引言 1.1课题背景 当今时代是飞速发展的信息时代。在各行各业中离不开信息处理&#xff0c;这正是计算机被广泛应用于信息管理系统的环境。计算机的最大好处在于利用它能够进行信息管理。使用计算机进行信息控制&#xff0c;不仅提高了工作效率&#xff0c;而且大大的提高了其…

江协科技STM32学习- P19 TIM编码器接口

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

Redis篇(Java操作Redis)

目录 讲解一&#xff1a;简介 讲解二&#xff1a;Jedis Github 一、创建项目、 二、添加依赖 三、配置文件 四、Java连接Redis 五、通过Redis连接池获取连接对象并操作服务器 六、封装JedisUtil对外提供连接对象获取方法 七、Java操作Redis五种数据类型 1. 连接与释放…

助农小程序|助农扶贫系统|基于java的助农扶贫系统小程序设计与实现(源码+数据库+文档)

助农扶贫系统小程序 目录 基于java的助农扶贫系统小程序设计与实现 一、前言 二、系统功能设计 三、系统实现 5.1.1 农户管理 5.1.2 用户管理 5.1.3 订单统计 5.2.1 商品信息管理 5.3.1 商品信息 5.3.2 订单信息 5.3.3 商品评价 5.3.4 商品退货 四、数据库设计 1、…

水波荡漾效果+渲染顺序+简单UI绘制

创建场景及布置 创建新场景Main,在Main场景中创建一个plane物体&#xff0c;命名为WaterWavePla,具体数值及层级面板排布如下&#xff1a; 编写脚本 创建一个文件夹&#xff0c;用于存放脚本&#xff0c;命名Scripts,创建一个子文件夹Effect,存放特效相关脚本&#xff0c;创建…

WAF,全称Web Application Firewall,好用WAF推荐

WAF&#xff0c;全称Web Application Firewall&#xff0c;即Web应用防火墙&#xff0c;是一种网络安全设备&#xff0c;旨在保护Web应用程序免受各种Web攻击&#xff0c;如SQL注入、跨站脚本&#xff08;XSS&#xff09;、跨站请求伪造&#xff08;CSRF&#xff09;等。 WAF通…

前端面试经验总结2(经典问题篇)

谈谈你对前端的理解 前端主要负责产品页面部分的实现&#xff0c;是最贴近于用户的程序员。 基本工作要求&#xff1a; 1.参与项目&#xff0c;通过与团队成员&#xff0c;UI设计&#xff0c;产品经理的沟通&#xff0c;快速高质量的实现效果图&#xff0c;并能够精确到1px 2.做…

数学建模研赛总结

目录 前言进度问题四分析问题五分析数模论文经验分享总结 前言 本文为博主数学建模比赛第五天的内容记录&#xff0c;希望所写的一些内容能够对大家有所帮助&#xff0c;不足之处欢迎大家批评指正&#x1f91d;&#x1f91d;&#x1f91d; 进度 今天已经是最后一天了&#xf…