带你实现OpenHarmony 微信聊天02

2024-11-24 16:14:06
3次阅读
0个评论

带你实现OpenHarmony 微信聊天02

接上一篇

前言

代码会统一放在码云上

案例目标

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

image-20240902230122815

实际效果

image-20240902231634937

案例功能

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

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

发送声音-功能演示

PixPin_2024-09-09_20-54-27

发送声音主要流程

image-20240909210811015


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

output

声明麦克风权限

image-20240909211939272

应用需要在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);
      }
    }
    
  3. 首页中 aboutToAppear 中调用

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

    image-20240909212046512

声音录制 AudioCapturer

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

output

封装录制声音类

根据上图的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;
  }
}

“按住说话” 发送录音

PixPin_2024-09-09_20-54-27


output

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

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

定义全局录音文件名

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

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

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

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

image-20240909214010076

声明停止录音方法

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

松开手指停止录音

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

image-20240909214836639

声明发送声音消息的方法

image-20240909214913820

// 生成声音消息
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)
}

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

image-20240909215957894

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

实现的思路为:

  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%")
}

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

image-20240909220252656

    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)
    }

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

image-20240909220457099

最后效果

PixPin_2024-09-09_22-04-01

录音生成的文件

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

image-20240909222855321

“按住说话” 取消发送

PixPin_2024-09-09_22-36-51

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

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

image-20240909224315034

播放声音消息 AudioRendererManager

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

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

output

封装声音播放类

根据上述的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

点击声音消息,播放声音

image-20240909230235647

声明播放录音的函数

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

给声音消息注册点击事件

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

image-20240909225939158

实时语音转文本

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

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

output

封装语音识别类

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

  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

语音识别业务流程

image-20240909232140090

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

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

”按住说话“ 语音识别

PixPin_2024-09-09_23-56-16

声明语音识别的文字状态

// 语音识别的文字
@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
    }

  })
}

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

image-20240909234345836


talkContainerBuilder内进行修改

image-20240909234518121

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

image-20240909234304591

松开手 结束语音识别

image-20240909234703000

松开手 发送文字消息

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

定义发送文字消息的方法

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

松开手 发送文字消息

image-20240909235517058

松开手 取消发送

PixPin_2024-09-10_00-00-11

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

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

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

image-20240909235855966

总结

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

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

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

源码地址

https://gitee.com/ukSir/hmchat2

参考链接

作者

作者:万少

链接:https://www.nutpi.net/

來源:坚果派 著作权归作者所有。

商业转载请联系作者获得授权,非商业转载请注明出处。

收藏00

登录 后评论。没有帐号? 注册 一个。