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

news2024/11/15 8:39:35

关键词: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.wav

     let 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.coloradmin.cn/o/2180691.html

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

相关文章

基于Python大数据可视化的短视频推荐系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

大模型压缩3种方式;模型大小的计算;知识蒸馏:利用教师的输入输出,训练调整学生的小模型

目录 大模型压缩3种方式 模型大小的计算 知识蒸馏:利用教师的输入输出,训练调整学生的小模型 最终学生学习多个教师的知识,学生强大 大模型压缩3种方式 模型大小的计算 知识蒸馏:利用教师的输入输出,训练调整学生的小模型

商务英语口语柯桥外语学习|ass是“屁股”,save是“救”,那 save my ass是什么意思?

有些人活着&#xff0c;屁股却已经“死”了 工作工作&#xff0c;上工就“坐”&#xff0c;“久坐”几乎是无法避免的事情&#xff0c;但你知道吗&#xff0c;长期久坐可能会患上死臀综合症&#xff08;Dormant Butt Syndrome&#xff09;&#xff01; 如果你坐久了就觉得屁股痛…

imagickd写shell的技术学习

前言 没想到吧哥们&#xff0c;imagickd也能写shell&#xff0c;真是学到了不少&#xff0c;下面会具体分析是如何写shell的 基础知识 Imagick类 参考官方手册https://www.php.net/manual/zh/class.imagick.php 重点关注他的构造方法 (PECL imagick 2, PECL imagick 3) …

基于vue框架的大学生兼职平台r8x19(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;企业,用户,兼职信息,报名记录,岗位类型,专业,取消记录,评价记录 开题报告内容 基于Vue框架的大学生兼职平台开题报告 一、开题报告名称 基于Vue框架的大学生兼职平台 二、研究背景与意义 2.1 研究背景 随着高等教育的普及和就业市…

https访问报错:net::ERR_CERT_DATE_INVALLD

目录 简介异常排查原因解决补充 简介 访问https资源出现报错 异常 排查 将地址拿到浏览器进行访问&#xff0c;可以很清晰的看到出现该问题的原因 原因 1、SSL证书已过期 2、服务器日期不准&#xff0c;不在证书有效期 解决 1、重新申请SSL证书&#xff0c;并配置 2、校正…

平安养老险深圳分公司积极开展“金融教育宣传月”活动,展现金融为民新风尚

2024年9月&#xff0c;平安养老险深圳分公司以“金融为民谱新篇&#xff0c;守护权益防风险”为主题&#xff0c;正式启动2024年“金融教育宣传月”活动&#xff0c;通过多样化开展进乡村、进商圈、进企业等宣传教育活动&#xff0c;将金融消保知识送达广大消费者身边&#xff…

Linux使用systemd安排定期任务的操作详解

systemd 定时器是一种替代传统 cron 的方法&#xff0c;用于安排定时任务。 systemd 定时器由两部分组成&#xff1a;一个 .service 文件和一个 .timer 文件。.service 文件定义了要执行的任务&#xff0c;而 .timer 文件设定了何时执行这个任务。 通常位于 /etc/systemd/syste…

使用rsync+jenkins实现服务自动部署全流程

项目背景&#xff1a;城市政务云服务器没有上k8s&#xff0c;所有后端服务都是原始方式部署启动 &#xff08;java -jar xxx.jar&#xff09;&#xff0c;那么有没有方式简化部署难度&#xff0c;实现自动部署&#xff1f;当然是有的&#xff0c;下面详细介绍&#xff08;以Cen…

开源实战分享 | 新书:《大型语言模型实战手册》随书代码分享

《大型语言模型实战手册》(英文版)目前电子版在亚马逊有售&#xff0c;纸质版预计在2024年10月15日开售。该书通过超过275张定制插图&#xff0c;深入探索大型语言模型的世界&#xff0c;为Python开发者提供使用大型语言模型所需的实用工具和概念。 如果对于插图没有特别执念的…

Jupyter的使用分享

文章目录 碎碎念安装方法1.安装Anaconda方法2.通过库的安装方式 启动使用教程1.指定目录打开2.启动后的简单使用 小结 碎碎念 前情提示 之前与许多小伙伴交流的时候&#xff0c;发现大家对于pycharm更容易上手&#xff08;可能是比较好设置中文的原因&#xff09;&#xff0c;在…

【HTML】img标签和超链接标签

文章目录 img 标签src 属性alt 属性title 属性width/height 属性border 属性 超链接标签&#xff1a;a表格标签合并单元格 img 标签 img 是一个单标签 src 属性 img 标签必须搭配 src 使用&#xff08;指定图片的路径&#xff09; 相对路径&#xff1a; ./xxx.png./img/xxx.…

MyBatis的注入问题

对之前文章的补充&#xff1a;MyBatis中的#{}与${}注入问题----原文链接 前言&#xff1a; MyBatis是一个流行的Java持久层框架&#xff0c;用于将对象与数据库中的数据进行映射。然而&#xff0c;如果不当使用&#xff0c;MyBatis也可能受到诸如SQL注入这类的安全问题的影响。…

60天持仓法则:Aberration策略如何实现市场盈利

近期&#xff0c;我们频繁探讨的焦点多集中于短线交易的策略与技巧。今天&#xff0c;让我们转换视角&#xff0c;来聊聊中长线交易策略。中长线交易通常需要交易员有充足的耐心和严格的风控管理能力&#xff0c;才能在多变的市场里赢取利益。在中长线交易中趋势仍然是分析重点…

为什么做谷歌seo廉价服务无法带来真正的效果?

谷歌SEO是一个复杂且技术含量高的过程&#xff0c;涉及到人力、技术、以及外链资源等多个方面。这些元素的组合使得SEO服务不可能是廉价的。如果有人向您推荐廉价的SEO服务&#xff0c;您需要保持警惕&#xff0c;因为这样的服务通常效果甚微&#xff0c;甚至可能对您的网站造成…

华为OD机试 - 模拟商场优惠打折(Python/JS/C/C++ 2024 E卷 200分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试真题&#xff08;Python/JS/C/C&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加入华为OD刷题交流群&#xff0c;…

CHI write 传输——CHI(5)

上篇介绍了dataless的操作类型&#xff0c;本篇我们来介绍一下write 一、Write 操作概览 cache stash &#xff1a;一种投机行为&#xff0c;通过在其未来的使用点附近分配一个cacheline来提高系统性能&#xff0c;因为可以减少使用数据时的内存访问延迟 二、CopyBack CopyB…

CRM如何助力企业内部高效管理?

企业内部的高效管理不仅是提升竞争力的关键&#xff0c;也是实现企业可持续发展的基石。客户关系管理&#xff08;CRM&#xff09;系统&#xff0c;作为连接客户与企业内部流程的重要桥梁&#xff0c;其在促进企业内部高效管理方面的作用日益凸显。通过自动化工作流程、跨部门信…

19.第二阶段x86游戏实战2-寻找寻路call

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要…

使用powershell的脚本报错:因为在此系统中禁止执行脚本

1.添加powershell功能环境&#xff1a; 2.启动powershell的执行策略 因为在此系统中禁止执行脚本。 set-executionpolicy unrestricted