我写个HarmonyOS Next版本的微信聊天03-完结篇

简介: 我写个HarmonyOS Next版本的微信聊天03-完结篇

我写个HarmonyOS Next版本的微信聊天02-完结篇

接上一篇

前言

代码会统一放在码云上

案例目标

这个是安卓手机上的真正的微信聊天界面功能效果

实际效果

案例功能

上一篇,已经实现了以下功能

  • 页面沉浸式
  • 聊天内容滚动
  • 输入框状态切换
  • 聊天信息框宽度自适应
  • 输入法避让
  • canvas声纹 按住说话
  • 发送文字
  • 录音-发送语音
  • 语音消息根据时长自动宽度
  • 手势坐标检测取消发送-语音转文字
  • 声音播放-语音消息
  • AI 语音转文字

发送声音-功能演示

36ba5c609382d4b93ba71e4b64c9a18f.png

发送声音主要流程


发送声音结合UI交互-主要流程

声明麦克风权限

应用需要在module.json5配置文件的requestPermissions标签中声明权限。

属性名称 含义 是否可缺省
name 标识需要使用的权限名称。取值范围请参考应用权限列表 该标签不可缺省,且必须为系统定义权限或definePermissions中定义的权限。
reason 标识申请权限的原因,取值需要采用资源引用格式,以适配多语种。 **说明:**当申请的权限为user_grant权限时,该字段必填,否则不允许在应用市场上架。
usedScene 标识权限使用的场景,包含abilities和when两个子标签。- - when:表示调用时机,支持的取值包括inuse(使用时)和always(始终)。 当申请的权限为user_grant权限时,abilities标签在hap中必填,when标签可选。


\entry\src\main\module.json5

requestPermissions

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

\entry\src\main\resources\base\element\string.json

$string:voice_reason"

{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "label"
    },
    {
      "name": "voice_reason",
      "value": "用于获取用户的录音"
    }
  ]
}

封装申请权限的工具类

权限工具类的主要功能为:

  1. 检测是否已经申请相关权限
  2. 申请相关权限

步骤:

  1. 新建文件 \entry\src\main\ets\utils\permissionMananger.ets
  2. 实现以下代码
// 导入必要的模块,包括权限管理相关的功能
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
export class PermissionManager {
  // 静态方法用于检查给定的权限是否已经被授予
  static checkPermission(permissions: Permissions[]): boolean {
    // 创建一个访问令牌管理器实例
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // 初始化tokenID为0,稍后将获取真实的tokenID
    let tokenID: number = 0;
    // 获取本应用的包信息
    const bundleInfo =
      bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    // 设置tokenID为应用的访问令牌ID
    tokenID = bundleInfo.appInfo.accessTokenId;
    // 如果没有传入任何权限,则返回false表示没有权限
    if (permissions.length === 0) {
      return false;
    } else {
      // 检查所有请求的权限是否都被授予
      return permissions.every(permission =>
      abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED ===
      atManager.checkAccessTokenSync(tokenID, permission)
      );
    }
  }
  // 异步静态方法用于请求用户授权指定的权限
  static async requestPermission(permissions: Permissions[]): Promise<boolean> {
    // 创建一个访问令牌管理器实例
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // 获取上下文(这里假设getContext是一个可以获取到UI能力上下文的方法)
    let context: Context = getContext() as common.UIAbilityContext;
    // 请求用户授权指定的权限
    const result = await atManager.requestPermissionsFromUser(context, permissions);
    // 检查请求结果是否成功(authResults数组中每个元素都应该是0,表示成功)
    return !!result.authResults.length && result.authResults.every(authResults => authResults === 0);
  }
}

  1. 首页中 aboutToAppear 中调用
// 页面开始显示触发
async aboutToAppear() {
  // 1 检查是否有权限
  // 2 如果没有权限 再去申请权限
  const permissionList: Permissions[] = ["ohos.permission.MICROPHONE"]
  const result = await PermissionManager.checkPermission(permissionList)
  if (!result) {
    PermissionManager.requestPermission(permissionList)
  }
}

  1. 最后效果

声音录制 AudioCapturer

AudioCapturer是音频采集器,用于录制PCM(Pulse Code Modulation)音频数据

封装录制声音类

根据上图的AudioCapturer使用流程,我们将封装 AudioCapturer录音类,主要有三个核心方法:

  1. 创建 AudioCapturer实例
  2. 开始录音
  3. 停止录音

\entry\src\main\ets\utils\AudioCapturerManager.ets

// 导入音频处理模块
import { audio } from '@kit.AudioKit';
// 导入文件系统模块
import fs from '@ohos.file.fs';
// 定义一个接口来描述录音文件的信息
export interface RecordFile {
  recordFilePath: string, // 录音文件的路径
  startRecordTime: number, // 开始录音的时间戳
  endRecordTime: number // 结束录音的时间戳
}
// 定义一个管理音频录制的类
export class AudioCapturerManager {
  // 静态属性,用于存储当前的音频捕获器实例
  static audioCapturer: audio.AudioCapturer | null = null;
  // 静态私有属性,用于存储录音文件的路径
  private static recordFilePath: string = "";
  // 静态私有属性,用于存储开始录音的时间戳
  private static startRecordTime: number = 0;
  // 静态私有属性,用于存储结束录音的时间戳
  private static endRecordTime: number = 0;
  // 静态异步方法,用于创建音频捕获器实例
  static async createAudioCapturer() {
    if (AudioCapturerManager.audioCapturer) {
      return AudioCapturerManager.audioCapturer
    }
    // 设置音频流信息配置
    let audioStreamInfo: audio.AudioStreamInfo = {
      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // 设置采样率为16kHz
      channels: audio.AudioChannel.CHANNEL_1, // 设置单声道
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 设置样本格式为16位小端
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 设置编码类型为原始数据
    };
    // 设置音频捕获信息配置
    let audioCapturerInfo: audio.AudioCapturerInfo = {
      source: audio.SourceType.SOURCE_TYPE_MIC, // 设置麦克风为音频来源
      capturerFlags: 0 // 捕获器标志,此处为默认值
    };
    // 创建音频捕获选项对象
    let audioCapturerOptions: audio.AudioCapturerOptions = {
      streamInfo: audioStreamInfo, // 使用上面定义的音频流信息
      capturerInfo: audioCapturerInfo // 使用上面定义的音频捕获信息
    };
    // 创建音频捕获器实例
    AudioCapturerManager.audioCapturer = await audio.createAudioCapturer(audioCapturerOptions);
    // 返回创建的音频捕获器实例
    return AudioCapturerManager.audioCapturer;
  }
  // 静态异步方法,用于启动录音过程
  static async startRecord(fileName: string) {
    await AudioCapturerManager.createAudioCapturer()
    // 记录开始录音的时间戳
    AudioCapturerManager.startRecordTime = Date.now();
    try {
      // 初始化缓冲区大小
      let bufferSize: number = 0;
      // 定义一个内部类来设置写入文件时的选项
      class Options {
        offset?: number; // 文件写入位置偏移量
        length?: number; // 写入数据的长度
      }
      // 获取应用的文件目录路径
      let path = getContext().filesDir;
      // 设置录音文件的完整路径
      let filePath = `${path}/${fileName}.wav`;
      AudioCapturerManager.recordFilePath = filePath;
      // 打开或创建录音文件
      let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      // 定义一个读取数据的回调函数
      let readDataCallback = (buffer: ArrayBuffer) => {
        // 创建一个写入文件的选项对象
        let options: Options = {
          offset: bufferSize, // 文件当前位置偏移量
          length: buffer.byteLength // 数据长度
        };
        // 将数据写入文件
        fs.writeSync(file.fd, buffer, options);
        // 更新缓冲区大小
        bufferSize += buffer.byteLength;
      };
      // 给音频捕获器实例注册读取数据的事件监听器
      AudioCapturerManager.audioCapturer?.on('readData', readDataCallback);
      // 开始录音
      AudioCapturerManager.audioCapturer?.start();
      // 返回录音文件的路径
      return filePath;
    } catch (e) {
      // 如果出现异常,返回空字符串
      return "";
    }
  }
  // 静态异步方法,用于停止录音过程
  static async stopRecord() {
    // 停止音频捕获器的工作
    await AudioCapturerManager.audioCapturer?.stop();
    // 释放音频捕获器的资源
    await AudioCapturerManager.audioCapturer?.release();
    // 清除音频捕获器实例
    AudioCapturerManager.audioCapturer = null;
    // 记录结束录音的时间戳
    AudioCapturerManager.endRecordTime = Date.now();
    // 创建并返回一个包含录音文件信息的对象
    const recordFile: RecordFile = {
      recordFilePath: AudioCapturerManager.recordFilePath,
      startRecordTime: AudioCapturerManager.startRecordTime,
      endRecordTime: AudioCapturerManager.endRecordTime,
    };
    // 返回记录文件信息
    return recordFile;
  }
}

“按住说话” 发送录音

36ba5c609382d4b93ba71e4b64c9a18f.png


这里我们先实现最简单的录音功能,转换文本或者取消发送下一个环节再实现

  1. 当长按 按住说话时,便开始录音
  2. 当直接松开手指时,便停止录音
  3. 同时构造声音消息,显示在聊天面板上

定义全局录音文件名

// 录音文件名称
  recordFileName: string = ""

首页中定义开始录音的方法

// 开始录音
onStartRecord = () => {
  // 文件名  唯一
  this.recordFileName = Date.now().toString()
  AudioCapturerManager.startRecord(this.recordFileName)
}

长按 "按住说话",开始录音

声明停止录音方法

// 结束录音
  stopRecord = async () => {
    // res 记录录音文件的路径、时长等信息  这里返回是为了实现 发送录音消息
    const res = await AudioCapturerManager.stopRecord()
    return res
  }

松开手指停止录音

onPressTalk 中的松开手指事件 TouchType.Up中停止录音

声明发送声音消息的方法

// 生成声音消息
postVoice = (res: RecordFile) => {
  // 录音时长  录音结束时间-开始录音时间
  const duration = Math.ceil((res.endRecordTime - res.startRecordTime) / 1000)
  //   生成消息文件
  const voiceChat = new ChatMessage(MessageType.voice, res.recordFilePath, duration)
  //   插入到消息数组中
  this.chatList.push(voiceChat)
}

定义渲染声音消息的自定义构建函数

该部分代码 可以根据声音消息的时长,动态设置消息的宽度

实现的思路为:

  1. 如果 80 + 时长*3 大于屏幕的一半,那么最大就是屏幕的一半
  2. 否则 宽度就是 80+时长*3
.width(
  80 + duration * 3 > px2vp(display.getDefaultDisplaySync().width / 2) ?
  px2vp(display.getDefaultDisplaySync().width / 2) :
  80 + duration * 3
          )
  
/**
 * 声音消息 结构
 * @param fileName 录音的路径-后续做点击播放使用
 * @param time 发送消息的时间 如 22:21
 * @param duration 消息的时长 如 5s
 * @param index 该消息在数组中的索引 后续做声音转文本使用
 */
@Builder
chatVoiceBuilder(fileName: string, time: string, duration: number, index: number) {
  Column({ space: 5 }) {
    Text(time)
      .width("100%")
      .textAlign(TextAlign.Center)
      .fontColor("#666")
      .fontSize(14)
    Row() {
      Flex({ justifyContent: FlexAlign.End }) {
        Column({ space: 10 }) {
          Row() {
            // 声音时长
            Text(`${duration}"`)
              .padding(11)
            Image($r("app.media.voice"))
              .width(20)
              .rotate({
                angle: 180
              })
              .margin({ right: 12 })
            Text()
              .width(10)
              .height(10)
              .backgroundColor("#93EC6C")
              .position({
                right: 0,
                top: 15
              })
              .translate({
                x: 5,
              })
              .rotate({
                angle: 45
              });
          }
          .backgroundColor("#93EC6C")
          .margin({ right: 15 })
          .borderRadius(5)
          // 根据声音时长,动态计算声音长度
          // 如果 80 + 时长*3  大于屏幕的一半,那么最大就是屏幕的一半
          // 否则 宽度就是 80+时长*3
          .width(80 + duration * 3 > px2vp(display.getDefaultDisplaySync().width / 2) ?
          px2vp(display.getDefaultDisplaySync().width / 2) : 80 + duration * 3)
          .justifyContent(FlexAlign.End)
        }
        .alignItems(HorizontalAlign.End)
        Image($r("app.media.avatar"))
          .width(40)
          .aspectRatio(1)
      }
      .width("100%")
      .padding({
        left: 40
      })
    }
  }
  .width("100%")
}

遍历消息数组时,动态渲染文本消息和声音消息

if (item.type === MessageType.text) {
      this.chatTextBuilder(item.content, item.time)
    } else if (item.type === MessageType.voice) {
      this.chatVoiceBuilder(item.content, item.time, item.duration!, index)
    }

松开手指停止录音 同时发送声音消息

最后效果

334bb62736f9a76bdc66778d8b6f2864.png

录音生成的文件

生成的录音文件都放在这里了 /data/app/el2/100/base/com.example.你项目的包名/haps/entry/files

“按住说话” 取消发送

8b2028b15bc667ce1a54acff8c1677e9.png

该功能主要的实现流程是:

当长按 “按住说话” ,并且判断手指是否移动到了 X这个功能在上一章已经实现了),如果是,则什么都不做即可

播放声音消息 AudioRendererManager

AudioRenderer是音频渲染器,用于播放PCM(Pulse Code Modulation)音频数据,相比AVPlayer而言,可以在输入前添加数据预处

理,更适合有音频开发经验的开发者,以实现更灵活的播放功能。

封装声音播放类

根据上述的AudioRenderer流程图,我们将封装AudioRendererManager声音播放类,实现了核心的五个功能:

  1. 初始化AudioRenderer实例
  2. 开始播放声音
    当播放完毕时,会自动停止播放和释放资源
  3. 暂停播放声音
  4. 停止播放声音
  5. 释放AudioRenderer相关资源

\entry\src\main\ets\utils\AudioRendererManager.ets


import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
class Options {
  offset?: number;
  length?: number;
}
class AudioRendererManager {
  /**
   * 音频播放实例
   */
  private static audioRender: audio.AudioRenderer | null = null
  /**
   * 初始化
   */
  static async init(fileName: string) {
    try {
      let bufferSize: number = 0;
      let 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 // 编码格式
      }
      let audioRendererInfo: audio.AudioRendererInfo = {
        usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型
        rendererFlags: 0 // 音频渲染器标志
      }
      let audioRendererOptions: audio.AudioRendererOptions = {
        streamInfo: audioStreamInfo,
        rendererInfo: audioRendererInfo
      }
      let path = getContext().filesDir;
      // let filePath = `${path}/${fileName}.wav`;
      let file: fileIo.File = fileIo.openSync(fileName, fileIo.OpenMode.READ_ONLY);
      const fileSize = fileIo.statSync(file.path).size
      let writeDataCallback = (buffer: ArrayBuffer) => {
        let options: Options = {
          offset: bufferSize,
          length: buffer.byteLength
        }
        fileIo.readSync(file.fd, buffer, options);
        bufferSize += buffer.byteLength;
        //   自动停止
        if (bufferSize >= fileSize) {
          AudioRendererManager.stop()
            .then(() => {
              AudioRendererManager.release()
            })
        }
      }
      AudioRendererManager.audioRender = await audio.createAudioRenderer(audioRendererOptions)
      AudioRendererManager.audioRender.on("writeData", writeDataCallback)
    } catch (e) {
      console.log("e", e.message, e.code)
    }
  }
  /** 播放 */
  static async start() {
    // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染
    await AudioRendererManager.audioRender?.start();
  }
  /** 暂停播放*/
  static async pause() {
    await AudioRendererManager.audioRender?.pause()
  }
  /** 结束播放 */
  static async stop() {
    await AudioRendererManager.audioRender?.stop()
  }
  /** 释放资源 */
  static async release() {
    await AudioRendererManager.audioRender?.release()
  }
}


export default AudioRendererManager

点击声音消息,播放声音

声明播放录音的函数

// 播放聊天记录中的录音
  startPlayRecord = async (fileName: string) => {
    // console.log("fileName",fileName)
    //   1 播放录音的实例的初始化
    await AudioRendererManager.init(fileName)
    //   2 播放录音
    AudioRendererManager.start()
  }

给声音消息注册点击事件

事件触发了调用播放语音的方式 startPlayRecord

实时语音转文本

Core Speech Kit(基础语音服务)集成了语音类基础AI能力,包括文本转语音(TextToSpeech)及语音识别(SpeechRecognizer)能力,便于用户与设备进行互动,实现将实时输入的语音与文本之间相互转换

实时语音识别将一段音频信息(短语音模式不超过60s,长语音模式不超过8h)转换为文本。

封装语音识别类

根据以上步骤,我们可以将语音识别拆分成核心功能:

  1. 创建语音识别引擎 createEngine
  2. 设置监听的回调 setListener
  3. 开始监听 startListening
  4. 取消识别 cancel
  5. 结束识别 finish
  6. 释放资源 shutDown

其中针对实际业务,利用上述功能额外组合封装了两个方法

  1. 停止并且释放资源 release
  2. 一键开启识别 init

\entry\src\main\ets\utils\SpeechRecognizerManager.ets

import { speechRecognizer } from '@kit.CoreSpeechKit';
import { fileIo } from '@kit.CoreFileKit';
class SpeechRecognizerManager {
  /**
   * 语种信息
   * 语音模式:长
   */
  private static extraParam: Record<string, Object> = { "locate": "CN", "recognizerMode": "long" };
  private static initParamsInfo: speechRecognizer.CreateEngineParams = {
    /**
     * 地区信息
     * */
    language: 'zh-CN',
    /**
     * 离线模式:1
     */
    online: 1,
    extraParams: this.extraParam
  };
  /**
   * 引擎
   */
  private static asrEngine: speechRecognizer.SpeechRecognitionEngine | null = null
  /**
   * 录音结果
   */
  static speechResult: speechRecognizer.SpeechRecognitionResult | null = null
  /**
   * 会话ID
   */
  private static sessionId: string = "asr" + Date.now()
  /**
   * 创建引擎
   */
  private static async createEngine() {
    // 设置创建引擎参数
    SpeechRecognizerManager.asrEngine = await speechRecognizer.createEngine(SpeechRecognizerManager.initParamsInfo)
  }
  /**
   * 设置回调
   */
  private static setListener(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => {
  }) {
    // 创建回调对象
    let setListener: speechRecognizer.RecognitionListener = {
      // 开始识别成功回调
      onStart(sessionId: string, eventMessage: string) {
      },
      // 事件回调
      onEvent(sessionId: string, eventCode: number, eventMessage: string) {
      },
      // 识别结果回调,包括中间结果和最终结果
      onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult) {
        SpeechRecognizerManager.speechResult = result
        callback && callback(result)
      },
      // 识别完成回调
      onComplete(sessionId: string, eventMessage: string) {
      },
      // 错误回调,错误码通过本方法返回
      // 如:返回错误码1002200006,识别引擎正忙,引擎正在识别中
      // 更多错误码请参考错误码参考
      onError(sessionId: string, errorCode: number, errorMessage: string) {
      },
    }
    // 设置回调
    SpeechRecognizerManager.asrEngine?.setListener(setListener);
  }
  /**
   * 开始监听
   * */
  static startListening() {
    // 设置开始识别的相关参数
    let recognizerParams: speechRecognizer.StartParams = {
      // 会话id
      sessionId: SpeechRecognizerManager.sessionId,
      // 音频配置信息。
      audioInfo: {
        // 音频类型。 当前仅支持“pcm”
        audioType: 'pcm',
        // 音频的采样率。 当前仅支持16000采样率
        sampleRate: 16000,
        // 音频返回的通道数信息。 当前仅支持通道1。
        soundChannel: 1,
        // 音频返回的采样位数。 当前仅支持16位
        sampleBit: 16
      },
      //   录音识别
      extraParams: {
        // 0 实时录音识别
        "recognitionMode": 0,
        //   最大支持音频时长
        maxAudioDuration: 60000
      }
    }
    // 调用开始识别方法
    SpeechRecognizerManager.asrEngine?.startListening(recognizerParams);
  };
  /**
   * 取消识别
   */
  static cancel() {
    SpeechRecognizerManager.asrEngine?.cancel(SpeechRecognizerManager.sessionId)
  }
  /**
   * 结束识别
   */
  static finish() {
    SpeechRecognizerManager.asrEngine?.finish(SpeechRecognizerManager.sessionId)
  }
  /**
   * 释放ai语音转文字引擎
   */
  static shutDown() {
    SpeechRecognizerManager.asrEngine?.shutdown()
  }
  /**
   * 停止并且释放资源
   */
  static async release() {
    SpeechRecognizerManager.cancel()
    SpeechRecognizerManager.shutDown()
  }
  /**
   * 初始化ai语音转文字引擎 实现一键开启语音识别
   */
  static async init(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => {
  }) {
    await SpeechRecognizerManager.createEngine()
    SpeechRecognizerManager.setListener(callback)
    SpeechRecognizerManager.startListening()
  }
}


export default SpeechRecognizerManager

语音识别业务流程

从上可以看到,我们要做的流程是:

  1. 在开始 按住说话 时,也直接开启实时语音识别
  2. 当手指移向 时,显示实时识别的文字
  3. 如果这个时候松开手,那么发送的是文字而不是语音

”按住说话“ 语音识别

4af7a24674b4c9ade4cc79437d5d1567.png

声明语音识别的文字状态

// 语音识别的文字
@State
voiceToText: string = ""

声明语音识别函数

// 开启ai实时转换声音
onStartSpeechRecognize = () => {
  // 如果你是完整的一句话,我把它拼接到 this.voiceToText 如果不是,实时显示
  // 缓存一段句子的变量
  let sentence = ""
  SpeechRecognizerManager.init((res) => {
    // console.log("res", JSON.stringify(res))
    //   isFinal  这一句话 你结束了没有
    //   isLast 这一段语音你结束了没有
    // this.voiceToText = res.result
    if (res.isFinal) {
      sentence += res.result
      this.voiceToText = sentence
    } else {
      this.voiceToText = sentence + res.result
    }
  })
}

设置转换的文字显示在绿色容器内


talkContainerBuilder内进行修改

按住说话 开始语音识别监听

松开手 结束语音识别

松开手 发送文字消息

本来是发送录音消息的,但是由于用户进行了语音转文字,所以此时直接松开手,便将语音转成的文字发送成文字消息

定义发送文字消息的方法

// 生成文字消息
postText = () => {
  //   生成消息文件
  const TextChat = new ChatMessage(MessageType.text, this.voiceToText)
  //   插入到消息数组中
  this.chatList.push(TextChat)
}

松开手 发送文字消息

松开手 取消发送

4b11a9dda885ecd326aeafb1c13ceea0.png

松开手时,一共有三种状态

  1. 直接发送语音
  2. 语音识别,发送文字
  3. 取消发送

现在来实现最后的 松开手,取消发送功能

总结

至此,该案例的功能已经全部完成

  • 页面沉浸式
  • 聊天内容滚动
  • 输入框状态切换
  • 聊天信息框宽度自适应
  • 输入法避让
  • canvas声纹 按住说话
  • 发送文字
  • 录音-发送语音
  • 语音消息根据时长自动宽度
  • 手势坐标检测取消发送-语音转文字
  • 声音播放-语音消息
  • AI 语音转文字

致自己,夜幕低垂,万籁俱寂,愿这份灵感之光伴随入梦,在未来的日子里,继续照亮探索之旅。感谢陪伴

源码地址

gitee.com/ukSir/hmcha…

参考链接

三文带你轻松上手鸿蒙的AI语音01-实时语音识别

三文带你轻松上手鸿蒙的AI语音02-声音文件转文本

三文带你轻松上手鸿蒙的AI语音03-文本合成声音

高质量 HarmonyOS 应用权限管控流程

相关实践学习
达摩院智能语音交互 - 声纹识别技术
声纹识别是基于每个发音人的发音器官构造不同,识别当前发音人的身份。按照任务具体分为两种: 声纹辨认:从说话人集合中判别出测试语音所属的说话人,为多选一的问题 声纹确认:判断测试语音是否由目标说话人所说,是二选一的问题(是或者不是) 按照应用具体分为两种: 文本相关:要求使用者重复指定的话语,通常包含与训练信息相同的文本(精度较高,适合当前应用模式) 文本无关:对使用者发音内容和语言没有要求,受信道环境影响比较大,精度不高 本课程主要介绍声纹识别的原型技术、系统架构及应用案例等。 讲师介绍: 郑斯奇,达摩院算法专家,毕业于美国哈佛大学,研究方向包括声纹识别、性别、年龄、语种识别等。致力于推动端侧声纹与个性化技术的研究和大规模应用。
目录
相关文章
|
25天前
|
API
HarmonyOS 3.1/4项目在DevEco Studio 5.0(HarmonyOS NEXT)版本下使用的问题
有读者在使用《鸿蒙HarmonyOS应用开发入门》书中的源码时遇到问题,主要原因是使用的DevEco Studio版本不同所致。本文提供了三种解决方案:1) 降级DevEco Studio至3.1版本;2) 按照5.0版本修改书中示例;3) 等待并使用《鸿蒙之光HarmonyOS NEXT原生应用开发入门》升级版书籍。
141 1
|
10天前
|
存储 JSON 开发工具
三、HarmonyOS NEXT应用开发:ArkTS工程目录结构(Stage模型)
本文介绍了HarmonyOS NEXT应用开发中ArkTS工程的目录结构(Stage模型),包括AppScope、entry、hvigor、oh_modules等主要目录及其作用。重点解析了entry目录下的src > main > resources目录结构,详细说明了base、限定符目录和rawfile的作用,以及如何引用资源文件。
47 1
|
29天前
|
JSON JavaScript 前端开发
harmony-chatroom 自研纯血鸿蒙OS Next 5.0聊天APP实战案例
HarmonyOS-Chat是一个基于纯血鸿蒙OS Next5.0 API12实战开发的聊天应用程序。这个项目使用了ArkUI和ArkTS技术栈,实现了类似微信的消息UI布局、输入框光标处插入文字、emoji表情图片/GIF动图、图片预览、红包、语音/位置UI、长按语音面板等功能。
62 2
|
1月前
|
UED
鸿蒙next版开发:相机开发-适配不同折叠状态的摄像头变更(ArkTS)
在HarmonyOS 5.0中,ArkTS提供了强大的相机开发能力,特别是针对折叠屏设备的摄像头适配。本文详细介绍了如何在ArkTS中检测和适配不同折叠状态下的摄像头变更,确保相机应用在不同设备状态下的稳定性和用户体验。通过代码示例展示了具体的实现步骤。
71 8
|
1月前
|
API 内存技术
鸿蒙next版开发:相机开发-拍照(ArkTS)
在HarmonyOS 5.0中,ArkTS提供了一套完整的API来管理相机功能,特别是拍照功能。本文详细介绍如何在ArkTS中实现拍照功能,包括导入接口、创建会话、配置会话、触发拍照及监听拍照输出流状态,并提供代码示例进行详细解读。通过本文,你将掌握如何在HarmonyOS 5.0中使用ArkTS实现高效的拍照功能。
87 7
|
1月前
|
前端开发 API
鸿蒙next版开发:相机开发-预览(ArkTS)
在HarmonyOS 5.0中,使用ArkTS进行相机预览是核心功能之一。本文详细介绍了如何使用ArkTS实现相机预览,包括导入相机接口、创建Surface、获取相机输出能力、创建会话并开始预览,以及监听预览输出状态等步骤,并提供了代码示例。通过本文,读者可以掌握在HarmonyOS 5.0中使用ArkTS进行相机预览的基本方法。
65 6
|
1月前
|
前端开发 开发者
鸿蒙next版开发:相机开发-元数据(ArkTS)
在HarmonyOS 5.0中,ArkTS新增了对相机元数据的访问能力,帮助开发者获取图像的详细信息。本文介绍了如何在ArkTS中获取和使用相机元数据,包括导入接口、创建元数据输出流、开启和停止元数据输出、监听元数据对象可用事件等步骤,并提供了详细的代码示例。
69 5
|
1月前
|
前端开发 API 开发者
鸿蒙next版开发:相机开发-录像(ArkTS)
在HarmonyOS 5.0中,ArkTS提供了一套完整的API来管理相机录像功能。本文详细介绍了如何在ArkTS中实现录像功能,包括导入接口、创建Surface、获取相机输出能力、创建会话并开始录像以及监听录像输出流状态,并提供了代码示例进行解读。希望本文能帮助开发者更好地利用ArkTS的相机录像功能。
55 5
|
1月前
|
API 开发者 内存技术
鸿蒙next版开发:相机开发-会话管理(ArkTS)
在HarmonyOS 5.0中,ArkTS提供了完整的API来管理相机会话,包括创建相机输入流、预览输出流、拍照输出流,配置和管理会话。本文详细介绍了相机会话管理的基础步骤和代码示例,涵盖会话创建、闪光灯和焦距配置及错误处理等内容,帮助开发者更好地利用ArkTS开发相机应用。
62 4
|
1月前
|
UED
鸿蒙next版开发:音频并发策略扩展(ArkTS)
在HarmonyOS 5.0中,音频并发策略通过ArkTS的AudioSessionManager接口管理多个音频流的交互和优先级。本文介绍了如何自定义音频焦点策略,包括激活、停用音频会话及注册回调函数,并提供了示例代码。适用于多媒体、通信和游戏应用。
58 4