我写个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 应用权限管控流程

相关实践学习
达摩院智能语音交互 - 声纹识别技术
声纹识别是基于每个发音人的发音器官构造不同,识别当前发音人的身份。按照任务具体分为两种: 声纹辨认:从说话人集合中判别出测试语音所属的说话人,为多选一的问题 声纹确认:判断测试语音是否由目标说话人所说,是二选一的问题(是或者不是) 按照应用具体分为两种: 文本相关:要求使用者重复指定的话语,通常包含与训练信息相同的文本(精度较高,适合当前应用模式) 文本无关:对使用者发音内容和语言没有要求,受信道环境影响比较大,精度不高 本课程主要介绍声纹识别的原型技术、系统架构及应用案例等。 讲师介绍: 郑斯奇,达摩院算法专家,毕业于美国哈佛大学,研究方向包括声纹识别、性别、年龄、语种识别等。致力于推动端侧声纹与个性化技术的研究和大规模应用。
目录
相关文章
|
2月前
|
编解码 API 数据安全/隐私保护
自学HarmonyOS Next记录:实现相册访问功能
最近我决定开发一个鸿蒙App,旨在提供更好的照片管理体验。通过使用PhotoAccessHelper API,我实现了访问、显示和管理设备相册中的照片。过程中遇到了权限不足的问题,通过在config.json中添加权限声明并编写权限检查代码得以解决。此外,我还实现了分页加载和展示照片详细信息等功能,提升了用户体验。这次开发不仅让我掌握了API的使用,也深刻体会到鸿蒙系统对用户隐私和数据安全的重视。 总结这次开发,我不仅学到了技术知识,还明白了开发者保护用户数据安全的责任。未来将继续探索更多功能,欢迎关注和收藏!
214 70
自学HarmonyOS Next记录:实现相册访问功能
|
1天前
|
缓存 Java 测试技术
【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
14 3
【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
|
1天前
|
存储 监控 算法
EMAS 性能分析全面适配HarmonyOS NEXT,开启原生应用性能优化新纪元
阿里云EMAS(Enterprise Mobile Application Studio,简称EMAS)性能分析现已全面适配华为HarmonyOS NEXT操作系统,为企业客户及开发者提供覆盖应用全生命周期的性能监测与优化解决方案,助力企业抢占鸿蒙生态先机,赋能开发者打造极致体验。
|
2月前
|
安全 数据安全/隐私保护 Android开发
HarmonyOS 5.0 Next实战应用开发—‘我的家乡’【HarmonyOS Next华为公司完全自研的操作系统】
HarmonyOS NEXT是华为自研的鸿蒙操作系统的重要版本更新,标志着鸿蒙系统首次完全脱离Linux内核及安卓开放源代码项目(AOSP),仅支持鸿蒙内核和鸿蒙系统的应用。该版本引入了“和谐美学”设计理念,通过先进的物理渲染引擎还原真实世界的光影色彩,为用户带来沉浸式体验。应用图标设计融合国画理念,采用留白和实时模糊技术展现中式美学。 HarmonyOS NEXT强化了设备间的协同能力,支持无缝切换任务,如在手机、平板或电脑间继续阅读文章或编辑文件。系统注重数据安全和隐私保护,提供数据加密和隐私权限管理功能。此外,它利用分布式技术实现跨设备资源共
148 15
HarmonyOS 5.0 Next实战应用开发—‘我的家乡’【HarmonyOS Next华为公司完全自研的操作系统】
|
2月前
|
存储 JavaScript 开发工具
基于HarmonyOS 5.0(NEXT)与SpringCloud架构的跨平台应用开发与服务集成研究【实战】
本次的.HarmonyOS Next ,ArkTS语言,HarmonyOS的元服务和DevEco Studio 开发工具,为开发者提供了构建现代化、轻量化、高性能应用的便捷方式。这些技术和工具将帮助开发者更好地适应未来的智能设备和服务提供方式。
76 8
基于HarmonyOS 5.0(NEXT)与SpringCloud架构的跨平台应用开发与服务集成研究【实战】
|
1月前
|
存储 JSON 区块链
【HarmonyOS NEXT开发——ArkTS语言】购物商城的实现【合集】
HarmonyOS应用开发使用@Component装饰器将Home结构体标记为一个组件,意味着它可以在界面构建中被当作一个独立的UI单元来使用,并且按照其内部定义的build方法来渲染具体的界面内容。txt:string定义了一个名为Data的接口,用于规范表示产品数据的结构。src:类型为,推测是用于引用资源(可能是图片资源等)的一种特定类型,用于指定产品对应的图片资源。txt:字符串类型,用于存放产品的文字描述,比如产品名称等相关信息。price:数值类型,用于表示产品的价格信息。
61 5
|
2月前
|
安全 API 数据安全/隐私保护
自学记录HarmonyOS Next DRM API 13:构建安全的数字内容保护系统
在完成HarmonyOS Camera API开发后,我深入研究了数字版权管理(DRM)技术。最新DRM API 13提供了强大的工具,用于保护数字内容的安全传输和使用。通过学习该API的核心功能,如获取许可证、解密内容和管理权限,我实现了一个简单的数字视频保护系统。该系统包括初始化DRM模块、获取许可证、解密视频并播放。此外,我还配置了开发环境并实现了界面布局。未来,随着数字版权保护需求的增加,DRM技术将更加重要。如果你对这一领域感兴趣,欢迎一起探索和进步。
96 18
|
1月前
|
开发工具 开发者 容器
【HarmonyOS NEXT开发——ArkTS语言】欢迎界面(启动加载页)的实现【合集】
从ArkTS代码架构层面而言,@Entry指明入口、@Component助力复用、@Preview便于预览,只是初窥门径,为开发流程带来些许便利。尤其动画回调与Blank组件,细节粗糙,后续定当潜心钻研,力求精进。”,字体颜色为白色,字体大小等设置与之前类似,不过动画配置有所不同,时长为。,不过这里没有看到额外的动画效果添加到这个特定的图片元素上(与前面带动画的元素对比而言)。这是一个显示文本的视图,文本内容为“奇怪的知识”,设置了字体颜色为灰色(的结构体,它代表了整个界面组件的逻辑和视图结构。
55 1
|
2月前
|
人工智能 自然语言处理 API
自学记录HarmonyOS Next的HMS AI API 13:语音合成与语音识别
在完成图像处理项目后,我计划研究HarmonyOS Next API 13中的AI语音技术,包括HMS AI Text-to-Speech和Speech Recognizer。这些API提供了强大的语音合成与识别功能,支持多语言、自定义语速和音调。通过这些API,我将开发一个支持语音输入与输出的“语音助手”原型应用,实现从语音指令解析到语音响应的完整流程。此项目不仅提高了应用的交互性,也为开发者提供了广阔的创新空间。未来,语音技术将在无障碍应用和智慧城市等领域展现巨大潜力。如果你也对语音技术感兴趣,不妨一起探索这个充满无限可能的领域。 (238字符)
120 11
|
2月前
|
存储 API 计算机视觉
自学记录HarmonyOS Next Image API 13:图像处理与传输的开发实践
在完成数字版权管理(DRM)项目后,我决定挑战HarmonyOS Next的图像处理功能,学习Image API和SendableImage API。这两个API支持图像加载、编辑、存储及跨设备发送共享。我计划开发一个简单的图像编辑与发送工具,实现图像裁剪、缩放及跨设备共享功能。通过研究,我深刻体会到HarmonyOS的强大设计,未来这些功能可应用于照片编辑、媒体共享等场景。如果你对图像处理感兴趣,不妨一起探索更多高级特性,共同进步。
89 11

热门文章

最新文章