鸿蒙应用开发-录音并使用WebSocket实现实时语音识别

news2024/11/15 15:34:20

功能介绍:

录音并实时获取RAW的音频格式数据,利用WebSocket上传数据到服务器,并实时获取语音识别结果,参考文档使用AudioCapturer开发音频录制功能(ArkTS),更详细接口信息请查看接口文档:AudioCapturer8+和@ohos.net.webSocket (WebSocket连接)。

知识点:

  1. 熟悉使用AudioCapturer录音并实时获取RAW格式数据。
  2. 熟悉使用WebSocket上传音频数据并获取识别结果。
  3. 熟悉对敏感权限的动态申请方式,本项目的敏感权限为MICROPHONE
  4. 关于如何搭建实时语音识别服务,可以参考我的另外一篇文章:《识别准确率竟如此高,实时语音识别服务》。

使用环境:

  • API 9
  • DevEco Studio 4.0 Release
  • Windows 11
  • Stage模型
  • ArkTS语言

所需权限:

  1. ohos.permission.MICROPHONE

效果图:
在这里插入图片描述

核心代码:

src/main/ets/utils/Permission.ets是动态申请权限的工具:

import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';

async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus;

  // 获取应用程序的accessTokenID
  let tokenId: number;
  try {
    let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (err) {
    console.error(`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`);
  }

  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (err) {
    console.error(`checkAccessToken failed, code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}

export async function checkPermissions(permission: Permissions): Promise<boolean> {
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permission);

  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    return true
  } else {
    return false
  }
}

src/main/ets/utils/Recorder.ets是录音工具类,进行录音和获取录音数据。

import audio from '@ohos.multimedia.audio';
import { delay } from './Utils';

export default class AudioCapturer {
  private audioCapturer: audio.AudioCapturer | undefined = undefined
  private isRecording: boolean = false
  private audioStreamInfo: audio.AudioStreamInfo = {
    samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // 音频采样率
    channels: audio.AudioChannel.CHANNEL_1, // 录音通道数
    sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
    encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW  // 音频编码类型
  }
  private audioCapturerInfo: audio.AudioCapturerInfo = {
    // 音源类型,使用SOURCE_TYPE_VOICE_RECOGNITION会有减噪功能,如果设备不支持,该用普通麦克风:SOURCE_TYPE_MIC
    source: audio.SourceType.SOURCE_TYPE_VOICE_RECOGNITION,
    capturerFlags: 0 // 音频采集器标志
  }
  private audioCapturerOptions: audio.AudioCapturerOptions = {
    streamInfo: this.audioStreamInfo,
    capturerInfo: this.audioCapturerInfo
  }

  // 初始化,创建实例,设置监听事件
  constructor() {
    // 创建AudioCapturer实例
    audio.createAudioCapturer(this.audioCapturerOptions, (err, capturer) => {
      if (err) {
        console.error(`创建录音器失败, 错误码:${err.code}, 错误信息:${err.message}`)
        return
      }
      this.audioCapturer = capturer
      console.info('创建录音器成功')
    });
  }

  // 开始一次音频采集
  async start(callback: (state: number, data?: ArrayBuffer) => void) {
    // 当且仅当状态为STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一时才能启动采集
    let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]
    if (stateGroup.indexOf(this.audioCapturer.state) === -1) {
      console.error('启动录音失败')
      callback(audio.AudioState.STATE_INVALID)
      return
    }
    // 启动采集
    await this.audioCapturer.start()
    this.isRecording = true
    let bufferSize = 1920
    // let bufferSize: number = await this.audioCapturer.getBufferSize();

    while (this.isRecording) {
      let buffer = await this.audioCapturer.read(bufferSize, true)
      if (buffer === undefined) {
        console.error('读取录音数据失败')
      } else {
        callback(audio.AudioState.STATE_RUNNING, buffer)
      }
    }
    callback(audio.AudioState.STATE_STOPPED)
  }

  // 停止采集
  async stop() {
    this.isRecording = false
    // 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
    if (this.audioCapturer.state !== audio.AudioState.STATE_RUNNING && this.audioCapturer.state !== audio.AudioState.STATE_PAUSED) {
      console.warn('Capturer is not running or paused')
      return
    }
    await delay(200)
    // 停止采集
    await this.audioCapturer.stop()
    if (this.audioCapturer.state.valueOf() === audio.AudioState.STATE_STOPPED) {
      console.info('录音停止')
    } else {
      console.error('录音停止失败')
    }
  }

  // 销毁实例,释放资源
  async release() {
    // 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release
    if (this.audioCapturer.state === audio.AudioState.STATE_RELEASED || this.audioCapturer.state === audio.AudioState.STATE_NEW) {
      return
    }
    // 释放资源
    await this.audioCapturer.release()
  }
}

还需要一些其他的工具函数src/main/ets/utils/Utils.ets,这个主要用于睡眠等待:

// 睡眠
export function delay(milliseconds : number) {
  return new Promise(resolve => setTimeout( resolve, milliseconds));
}

还需要在src/main/module.json5添加所需要的权限,注意是在module中添加,关于字段说明,也需要在各个的string.json添加:

    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:record_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      }
    ]

页面代码如下:

import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common';
import webSocket from '@ohos.net.webSocket';
import AudioCapturer from '../utils/Recorder';
import promptAction from '@ohos.promptAction';
import { checkPermissions } from '../utils/Permission';
import audio from '@ohos.multimedia.audio';

// 需要动态申请的权限
const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE'];
// 获取程序的上下文
const context = getContext(this) as common.UIAbilityContext;

@Entry
@Component
struct Index {
  @State recordBtnText: string = '按下录音'
  @State speechResult: string = ''
  private offlineResult = ''
  private onlineResult = ''
  // 语音识别WebSocket地址
  private asrWebSocketUrl = "ws://192.168.0.100:10095"
  // 录音器
  private audioCapturer?: AudioCapturer;
  // 创建WebSocket
  private ws;

  // 页面显示时
  async onPageShow() {
    // 判断是否已经授权
    let promise = checkPermissions(permissions[0])
    promise.then((result) => {
      if (result) {
        // 初始化录音器
        if (this.audioCapturer == null) {
          this.audioCapturer = new AudioCapturer()
        }
      } else {
        this.reqPermissionsAndRecord(permissions)
      }
    })
  }

  // 页面隐藏时
  async onPageHide() {
    if (this.audioCapturer != null) {
      this.audioCapturer.release()
    }
  }

  build() {
    Row() {
      RelativeContainer() {
        Text(this.speechResult)
          .id("resultText")
          .width('95%')
          .maxLines(10)
          .fontSize(18)
          .margin({ top: 10 })
          .alignRules({
            top: { anchor: '__container__', align: VerticalAlign.Top },
            middle: { anchor: '__container__', align: HorizontalAlign.Center }
          })
        // 录音按钮
        Button(this.recordBtnText)
          .width('90%')
          .id("recordBtn")
          .margin({ bottom: 10 })
          .alignRules({
            bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
            middle: { anchor: '__container__', align: HorizontalAlign.Center }
          })
          .onTouch((event) => {
            switch (event.type) {
              case TouchType.Down:
                console.info('按下按钮')
              // 判断是否有权限
                let promise = checkPermissions(permissions[0])
                promise.then((result) => {
                  if (result) {
                    // 开始录音
                    this.startRecord()
                    this.recordBtnText = '录音中...'
                  } else {
                    // 申请权限
                    this.reqPermissionsAndRecord(permissions)
                  }
                })
                break
              case TouchType.Up:
                console.info('松开按钮')
                // 停止录音
                this.stopRecord()
                this.recordBtnText = '按下录音'
                break
            }
          })
      }
      .height('100%')
      .width('100%')
    }
    .height('100%')
  }

  // 开始录音
  startRecord() {
    this.setWebSocketCallback()
    this.ws.connect(this.asrWebSocketUrl, (err) => {
      if (!err) {
        console.log("WebSocket连接成功");
        let jsonData = '{"mode": "2pass", "chunk_size": [5, 10, 5], "chunk_interval": 10, ' +
          '"wav_name": "HarmonyOS", "is_speaking": true, "itn": false}'
        // 要发完json数据才能录音
        this.ws.send(jsonData)
        // 开始录音
        this.audioCapturer.start((state, data) => {
          if (state == audio.AudioState.STATE_STOPPED) {
            console.info('录音结束')
            // 录音结束,要发消息告诉服务器,结束识别
            let jsonData = '{"is_speaking": false}'
            this.ws.send(jsonData)
          } else if (state == audio.AudioState.STATE_RUNNING) {
            // 发送语音数据
            this.ws.send(data, (err) => {
              if (err) {
                console.log("WebSocket发送数据失败,错误信息:" + JSON.stringify(err))
              }
            });
          }
        })
      } else {
        console.log("WebSocket连接失败,错误信息: " + JSON.stringify(err));
      }
    });
  }

  // 停止录音
  stopRecord() {
    if (this.audioCapturer != null) {
      this.audioCapturer.stop()
    }
  }

  // 绑定WebSocket事件
  setWebSocketCallback() {
    // 创建WebSocket
    this.ws = webSocket.createWebSocket();
    // 接收WebSocket消息
    this.ws.on('message', (err, value: string) => {
      console.log("WebSocket接收消息,结果如下:" + value)
      // 解析数据
      let result = JSON.parse(value)
      let is_final = result['is_final']
      let mode = result['mode']
      let text = result['text']
      if (mode == '2pass-offline') {
        this.offlineResult = this.offlineResult + text
        this.onlineResult = ''
      } else {
        this.onlineResult = this.onlineResult + text
      }
      this.speechResult = this.offlineResult + this.onlineResult
      // 如果是最后的数据就关闭WebSocket
      if (is_final) {
        this.ws.close()
      }
    });
    // WebSocket关闭事件
    this.ws.on('close', () => {
      console.log("WebSocket关闭连接");
    });
    // WebSocket发生错误事件
    this.ws.on('error', (err) => {
      console.log("WebSocket出现错误,错误信息: " + JSON.stringify(err));
    });
  }

  // 申请权限
  reqPermissionsAndRecord(permissions: Array<Permissions>): void {
    let atManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        if (grantStatus[i] === 0) {
          // 用户授权,可以继续访问目标操作
          console.info('授权成功')
          if (this.audioCapturer == null) {
            this.audioCapturer = new AudioCapturer()
          }
        } else {
          promptAction.showToast({ message: '授权失败,需要授权才能录音' })
          return;
        }
      }
    }).catch((err) => {
      console.error(`requestPermissionsFromUser failed, code is ${err.code}, message is ${err.message}`);
    })
  }
}

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

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

相关文章

如何推进制造业数字化转型?《制造业数字化转型白皮书》分享给你

分享一份《制造业数字化转型白皮书》给你&#xff0c;希望对你有所帮助&#xff01; 内容较长&#xff0c;防止后续找不到&#xff0c;建议先收藏&#xff01; 变局&#xff1a;数字经济浪潮“不期而至” 中国制造何去何从&#xff1f; VUCA&#xff08;不稳定 Volatile、不确…

超详细SpringMVC源码剖析

整体流程图 1.自定义视图(63~66) 视图解析过程 1.先到DispatcherServlet中央控制器, 根据视图解析的 优先级 执行对应的 视图解析器 Nullable protected View resolveViewName(String viewName, Nullable Map<String, Object> model,Locale locale, HttpServletReque…

美团面试一面凉经

1.自我介绍 2.科研项目提问 没咋准备&#xff0c;说的有点没逻辑 3.问论坛项目 为什么用Redis实现登录&#xff1f;能不能用其他方式实现&#xff1f; 1、Redis 具备高性能 假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢&#xff0c;因为是从硬盘上读取的。将…

Springboot+vue的企业质量管理系统(有报告)。Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue的企业质量管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09…

堆排序(六大排序)

前面博客已经分享过堆的知识了&#xff0c;今天我们来分享堆排序。 堆排序 堆排序(Heapsort)是指利用堆积树&#xff08;堆&#xff09;这种数据结构所设计的一种排序算法&#xff0c;它是选择排序的一种。它是通过堆来进行选择数据。 ★★★需要注意的是排升序要建大堆&#…

公众号超牛鼻的爆文仿写机器人,原创三篇只需6分钟,篇篇是爆文基因

大家好&#xff0c;我是大胡子&#xff0c;专注于RPA提效​&#xff0c;今天就介绍一款公众号超牛鼻的爆文仿写机器人​。 和以前的公众号爆文机器人不太一样&#xff0c;以前的爆文机器人需要手动插入图片、添加封面、插入话题&#xff0c;然后今天这个机器人就完全解决这几个…

Docker 哲学 - tmpfs 存储

tem&#xff1a;temporary 暂时的 背景&#xff1a;只有在 linux 有该种方式 If youre running Docker on Linux, you have a third option: tmpfs mounts. When you create a container with a tmpfs mount, the container can create files outside the containers writabl…

Acwing.1402 星空之夜(离散化)

题解 夜空深处&#xff0c;闪亮的星星以星群的形式出现在人们眼中&#xff0c;形态万千。 一个星群是指一组非空的在水平&#xff0c;垂直或对角线方向相邻的星星的集合。 一个星群不能是一个更大星群的一部分。 星群可能是相似的。 如果两个星群的形状、包含星星的数目相…

Matlab|基于两阶段鲁棒优化的微网电源储能容量优化配置

目录 主要内容 1.1 目标函数 1.2 约束条件 1.3 不确定变量 部分代码 结果一览 下载链接 主要内容 程序主要复现的是《考虑寿命损耗的微网电池储能容量优化配置》&#xff0c;解决微网中电源/储能容量优化配置的问题&#xff0c;即风电、光伏、储能以及燃气轮机…

java-基于springboot+vue房屋租赁信息管理系统功能介绍

开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 项目关键技术 1、JSP技术 JSP(Java…

61.旋转链表

给你一个链表的头节点 head &#xff0c;旋转链表&#xff0c;将链表每个节点向右移动 k 个位置。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], k 2 输出&#xff1a;[4,5,1,2,3]示例 2&#xff1a; 输入&#xff1a;head [0,1,2], k 4 输出&#xff1a;[2,0,1]…

沪漂8年回郑州三年如何走上创业之路

大家好&#xff0c;我是大牛&#xff0c;目前人在郑州。 现在标签是&#xff1a; 创业者&#x1f697;&#x1f438; (注册有自己的公司&#xff0c;主要是为了自己的产品和接外包项目)独立开发者&#x1f468;&#x1f3fb;&#x1f4bb; (有自己的小项目)数字游民&…

代码随想录算法训练营第四十六天|139.单词拆分、56. 携带矿石资源(第八期模拟笔试)

139.单词拆分 刷题https://leetcode.cn/problems/word-break/description/文章讲解https://programmercarl.com/0139.%E5%8D%95%E8%AF%8D%E6%8B%86%E5%88%86.html视频讲解https://www.bilibili.com/video/BV1pd4y147Rh/?vd_sourceaf4853e80f89e28094a5fe1e220d9062 题解&…

32 mars3d 官方 demo 可以跑起来, 但是自己拷贝的 demo 跑不起来

前言 这个问题是 同事碰到的一个问题 主要的影响因素在于 官方的 demo 从 mars3d-cesium 中暴露了一部分文件作为 http 服务, 然后 我们自己的 case 里面没有这部分服务, 然后 导致 js 访问不到 以及 大部分的 css, js, img 等等 静态资源 访问不到 Cesium is not define…

鸿蒙应用开发-录音保存并播放音频

功能介绍&#xff1a; 录音并保存为m4a格式的音频&#xff0c;然后播放该音频&#xff0c;参考文档使用AVRecorder开发音频录制功能(ArkTS)&#xff0c;更详细接口信息请查看接口文档&#xff1a;ohos.multimedia.media (媒体服务)。 知识点&#xff1a; 熟悉使用AVRecorder…

MySQL之MVCC如何实现可重复读和提交读

(/≧▽≦)/~┴┴ 嗨~我叫小奥 ✨✨✨ &#x1f440;&#x1f440;&#x1f440; 个人博客&#xff1a;小奥的博客 &#x1f44d;&#x1f44d;&#x1f44d;&#xff1a;个人CSDN ⭐️⭐️⭐️&#xff1a;Github传送门 &#x1f379; 本人24应届生一枚&#xff0c;技术和水平有…

商城小程序项目实现监控的可观测性最佳实践

前言 微信小程序是一种轻量级的应用程序&#xff0c;用户可以在微信内直接使用&#xff0c;无需下载安装。它具有独立的开发框架和生态系统&#xff0c;支持丰富的功能和交互&#xff0c;包括社交、购物、服务等。 观测云对微信小程序的监控能够实时收集性能指标、错误日志和…

BUUCTF-Misc14

[WUSTCTF2020]find_me1 1.打开附件 是一个学校的校徽 2.盲文解密 发现图片属性里的备注是一串盲文 用在线盲文解密 3.得到flag

使用U盘重装CentOS7系统

下载CentOS7 系统镜像 制作启动U盘之前&#xff0c;首先要准备一个系统镜像&#xff0c;这里我在CentOS官网直接下载镜像文件&#xff1a; CentOS官网 为了保证下载速度&#xff0c;这里我使用阿里云镜像下载&#xff1a; 阿里云镜像 如上图所示&#xff0c;我需要一个带UI界…

【微服务】Gateway

文章目录 1.基本介绍官方文档&#xff1a;https://springdoc.cn/spring-cloud-gateway/#gateway-starter1.引出网关2.使用网关服务架构图3.Gateway网络拓扑图&#xff08;背下来&#xff09;4.Gateway特性5.Gateway核心组件1.基本介绍2.断言3.过滤 6.Gateway工作机制 2.搭建Gat…