鸿蒙NEXT-录音播放

421 阅读8分钟

一,权限申请

默认应用等级和权限接口的等级是相对应的 应用等级成为APL 俗称就是一个权限等级的划分

应用等级(APL)划分为:

1.normal 这个标识普通应用

2.system basic 这个是系统服务

3.system_core 这个是核心系统服务 (这是操作系统能力 也就是你只要在鸿蒙中运行他就是支持的,但是目前看下来基本只会用到前面里两个等级)

权限等级(ACL):

1.normal

2.system basic

3.system_core (同理)

他们的等级是一一对应的 如果normal跨级访问system basic权限的接口

file_1740838346026_670.png 当应用需要访问用户的隐私信息或使用系统能力时,例如获取位置信息、访问日历、使用相机拍摄照片或录制视频等,应该向用户请求授权,这部分权限是user_grant权限。

权限申请流程

1.第一步

首先就需要看 你申请的这个权限是否支持跨等级访问 这个官方有一个acl名单 标明了你这个等级的app是否支持那些权限 一般我们的应用默认都是normal

2.第二步

如果是支持的 要看使用这个权限的授权方式是userAgent 还是 system_grant 如果是userAgent 需要弹出一个授权弹框去找用户交互 让用户来手动授权 如果是system_grant 这个这个权限的你就可以直接声明权限直接去使用 不需要弹框告诉用户

3.第三步

要在modle.JSON5 文件中去声明权限(类似请求的那个权限就是写在那个文件里,具体怎么写 要看文档)

4.第四步

要自动生成签名 不然会导致你不能使用一些权限(所以切记 这个必须要生成,不然默认为你没有这个权限去访问这个写内容 ,他就相当于一个token ,有token才能访问这个内容)

5.最后

接下来就是正常套路了 先判断他是否拥有这个权限 如果拥有 然后再去调用申请这个系统的权限接口,这个调用的时候如果时userAgent 需要弹窗

image.png

具体实现流程

封装的一个权限管理工具类

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { ENTRY_ABILITY_CONTEXT } from '../constants';

class Permission {
  // 请求用户授权
  async requestPermissions(permissions: Permissions[]) {
    // 程序访问控制提供程序的权限管理能力,包括鉴权、授权等。  实例
    const atManager = abilityAccessCtrl.createAtManager()
    const ctx = AppStorage.get<Context>(ENTRY_ABILITY_CONTEXT)
    if (ctx) {
      // 弹出窗口向用户请求权限
      const result = await atManager.requestPermissionsFromUser(ctx, permissions)
      // 返回 权限申请的结果
      return result.authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    }
    return  false
}

  // 打开权限设置 beta3
  async openPermissionSetting(permissions: Permissions[]) {
    const atManager = abilityAccessCtrl.createAtManager()
    const ctx = AppStorage.get<Context>(ENTRY_ABILITY_CONTEXT)
    if (ctx) {
      const authResults = await atManager.requestPermissionOnSetting(ctx, permissions)
      return authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    }
    return  false
}
}

export  const permission = new Permission()

弹窗询问要权限 和 二次确认

entry/src/main/ets/pages/Test01.ets

import { permission } from '../commons/utils/Permission'
import { Permissions } from '@kit.AbilityKit'
import { promptAction, router } from '@kit.ArkUI'

@Entry
@Component
struct Test01 {
  build() {
    Column() {
      Button("申请麦克风权限")
        .onClick(async () => {
          // 麦克风的权限数组
          const perlist: Permissions[] = ["ohos.permission.MICROPHONE"]
          // 第一次请求权限
          const res1 = await permission.requestPermissions(perlist)
          if (!res1) {
            //   如果首次没有授权  弹出确认窗  询问是否确定不授权
            const confrim = await promptAction.showDialog({
              title: "警告",
              message: "该模块必须要有麦克风权限,否则无法正常使用",
              buttons: [
                { text: "不允许", color: "#f00" },
                { text: "授权", color: "#000" }
              ]
            })

            //   判断此次用户的决定
            //   如果授权
            if (confrim.index === 1) {
              const res2 = await permission.openPermissionSetting(perlist)
              if (!res2) {
                // 不授权
                router.back()
              }
            } else {
              router.back()
            }

          }

        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

二,功能划分

开始录音

AudioCapturer状态变化示意图

使用on('stateChange')方法可以监听AudioCapturer的状态变化,每个状态对应值与说明见AudioState

entry/src/main/ets/pages/Test01.ets

完整代码

import { permission } from '../commons/utils/Permission'
import { Permissions } from '@kit.AbilityKit'
import { promptAction, router } from '@kit.ArkUI'
import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';

@Entry
@Component
struct Test01 {
  avRecorder: media.AVRecorder | undefined = undefined;
  @State
  text: string = ""

  build() {
    Column() {
      Text(this.text)
        .fontSize(30)

      Button("申请麦克风权限")
        .onClick(async () => {
          // 麦克风的权限数组
          const perlist: Permissions[] = ["ohos.permission.MICROPHONE"]
          // 第一次请求权限
          const res1 = await permission.requestPermissions(perlist)
          if (!res1) {
            //   如果首次没有授权  弹出确认窗  询问是否确定不授权
            const confrim = await promptAction.showDialog({
              title: "警告",
              message: "该模块必须要有麦克风权限,否则无法正常使用",
              buttons: [
                { text: "不允许", color: "#f00" },
                { text: "授权", color: "#000" }
              ]
            })

            //   判断此次用户的决定
            //   如果授权
            if (confrim.index === 1) {
              const res2 = await permission.openPermissionSetting(perlist)
              if (!res2) {
                // 不授权
                router.back()
              }
            } else {
              router.back()
            }

          }

        })

      Button("开始录音")
        .onClick(async () => {

          // 指定录音文件的沙箱存储路径
          const ctx = getContext(this)
          // cacheDir  容量有限制 满了 会覆盖
          // filesDir 不卸载一直存在
          const path = ctx.filesDir + `/1111.m4a`

          // 获得文件权限
          const file = fileIo.openSync(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)


          //   1 创建 avRecord
          this.avRecorder = await media.createAVRecorder();
          //   2 指定录音相关的配置信息
          await this.avRecorder.prepare({
            audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
            profile: {
              audioBitrate: 100000, // 音频比特率
              audioChannels: 2, // 音频声道数
              audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
              audioSampleRate: 48000, // 音频采样率
              fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
            },
            url: 'fd://' + file.fd,
          });

          //   3 开始录制
          await this.avRecorder.start()

          this.text = "开始录音"
        })

      Button("结束录音")
        .onClick(async () => {
          await this.avRecorder?.release()
          this.text = "结束录音"
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

播放录音

在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。

  • prepared状态: 通过调用createAudioRenderer()方法进入到该状态。
  • running状态: 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在paused状态和stopped状态通过调用start()方法进入此状态。
  • paused状态: 在running状态可以通过调用pause()方法暂停音频数据的播放并进入paused状态,暂停播放之后可以通过调用start()方法继续音频数据播放。
  • stopped状态: 在paused/running状态可以通过stop()方法停止音频数据的播放。
  • released状态: 在prepared、paused、stopped等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。
import { permission } from '../commons/utils/Permission'
import { Permissions } from '@kit.AbilityKit'
import { promptAction, router } from '@kit.ArkUI'
import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';

@Entry
@Component
struct Test01 {
  avRecorder: media.AVRecorder | undefined = undefined;
  avPlayer: media.AVPlayer | undefined = undefined
@State
  text: string = ""

      Button("播放录音")
        .onClick(async () => {
          //   单词朗读 在线
          //   现在 本地沙箱目录
          // 创建avPlayer实例对象
          this.avPlayer = await media.createAVPlayer();

          this.avPlayer.on("stateChange", (state: string) => {
            // type AVPlayerState = 'idle' | 'initialized' | 'prepared' | 'playing' | 'paused' | 'completed' | 'stopped' | 'released' | 'error';
            switch (state) {
              case "initialized":
                this.avPlayer?.prepare()
                break;
              case "prepared":
                this.avPlayer?.play()
                break;
              case "completed":
                this.avPlayer?.play()
                break;
              default:
                break;
            }

          })

          // 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例
          let path = getContext(this).filesDir + '/1111.m4a';
          let file = fileIo.openSync(path);

          this.avPlayer.url = 'fd://' + file.fd
          this.text = "开始播放"
        })
      Button("结束播放")
        .onClick(async () => {
          await this.avPlayer?.release()
          this.text = "结束播放"
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

声音大小 振幅 显示声纹

import { permission } from '../commons/utils/Permission'
import { Permissions } from '@kit.AbilityKit'
import { promptAction, router } from '@kit.ArkUI'
import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';

@Entry
@Component
struct Test01 {
  avRecorder: media.AVRecorder | undefined = undefined;
  avPlayer: media.AVPlayer | undefined = undefined
@State
  text: string = ""
  tid: number = -1
  @State
  amplitude: number = 1

  aboutToAppear() {

  }

  build() {
    Column({ space: 10 }) {

      Row({ space: 5 }) {
        ForEach(Array.from({ length: 30 }), () => {
          Text()
            .width(4)
            .height(40 * Math.random() * (this.amplitude / 20000))
            .backgroundColor(Color.Blue)
        })
      }
      .width(300)
      .justifyContent(FlexAlign.SpaceBetween)


      Text(this.text)
        .fontSize(30)

      Button("申请麦克风权限")
        .onClick(async () => {
          // 麦克风的权限数组
          const perlist: Permissions[] = ["ohos.permission.MICROPHONE"]
          // 第一次请求权限
          const res1 = await permission.requestPermissions(perlist)
          if (!res1) {
            //   如果首次没有授权  弹出确认窗  询问是否确定不授权
            const confrim = await promptAction.showDialog({
              title: "警告",
              message: "该模块必须要有麦克风权限,否则无法正常使用",
              buttons: [
                { text: "不允许", color: "#f00" },
                { text: "授权", color: "#000" }
              ]
            })

            //   判断此次用户的决定
            //   如果授权
            if (confrim.index === 1) {
              const res2 = await permission.openPermissionSetting(perlist)
              if (!res2) {
                // 不授权
                router.back()
              }
            } else {
              router.back()
            }

          }

        })

      Button("开始录音")
        .onClick(async () => {

          try { // 指定录音文件的沙箱存储路径
            const ctx = getContext(this)
            // cacheDir  容量有限制 满了 会覆盖
            // filesDir 不卸载一直存在
            const path = ctx.filesDir + `/1111.m4a`

            // 获得文件权限
            const file = fileIo.openSync(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)


            //   1 创建 avRecord
            this.avRecorder = await media.createAVRecorder();
            //   2 指定录音相关的配置信息
            await  this.avRecorder.prepare({
              audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
              profile: {
                audioBitrate: 100000, // 音频比特率
                audioChannels: 2, // 音频声道数
                audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
                audioSampleRate: 48000, // 音频采样率
                fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
              },
              url: 'fd://' + file.fd,
            });

            //   3 开始录制
            await  this.avRecorder.start()

            this.text = "开始录音"
          } catch (e) {
            console.log("e 开始录音", e.message, e.code)
          }
        })

      Button("获取声音的振幅")
        .onClick(() => {
          this.tid = setInterval(async () => {
            // 最大 30000
            // 最小 0
            this.amplitude = await this.avRecorder!.getAudioCapturerMaxAmplitude()
            if (this.amplitude > 20000) {
              this.amplitude = 20000
            } else if (this.amplitude < 500) {
              this.amplitude = 500
            }
            promptAction.showToast({ message: `${this.amplitude}` })
          }, 500)

        })
      Button("停止获取声音振幅")
        .onClick(() => {
          clearInterval(this.tid)
        })

      Button("结束录音")
        .onClick(async () => {
          await  this.avRecorder?.release()
          this.text = "结束录音"
        })

      Button("播放录音")
        .onClick(async () => {
          //   单词朗读 在线
          //   现在 本地沙箱目录
          // 创建avPlayer实例对象
          this.avPlayer = await media.createAVPlayer();

          this.avPlayer.on("stateChange", (state: string) => {
            // type AVPlayerState = 'idle' | 'initialized' | 'prepared' | 'playing' | 'paused' | 'completed' | 'stopped' | 'released' | 'error';
            switch (state) {
              case "initialized":
                this.avPlayer?.prepare()
                break;
              case "prepared":
                this.avPlayer?.play()
                break;
              case "completed":
                this.avPlayer?.play()
                break;
              default:
                break;
            }

          })

          // 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例
          let path = getContext(this).filesDir + '/1111.m4a';
          let file = fileIo.openSync(path);

          this.avPlayer.url = 'fd://' + file.fd
          this.text = "开始播放"
        })
      Button("结束播放")
        .onClick(async () => {
          await  this.avPlayer?.release()
          this.text = "结束播放"
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

创建数据库

TestDB.ets

import { relationalStore } from '@kit.ArkData'

@Entry
@Component
struct TestDB {
  store: relationalStore.RdbStore | null = null
@State
  text: string = ""

  build() {
    Column({ space: 10 }) {

      Text(this.text)
        .fontSize(30)
      Button("创建数据库")
        .onClick(async () => {
          this.store = await relationalStore.getRdbStore(getContext(this), {
            name: 'asdfsdfd.db',
            securityLevel: relationalStore.SecurityLevel.S1
          })
          this.text = "创建数据库成功"
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

创建数据表

import { relationalStore } from '@kit.ArkData'

/*
 * 自己理解透 手写
 * cv获取到
 * */
@Entry
@Component
struct TestDB {
  store: relationalStore.RdbStore | null = null
 tableName = 'article'
  @State
  text: string = ""
  // 获取仓库实例
  getStore = async () => {
    this.store = await relationalStore.getRdbStore(getContext(this), {
      name: 'interview_tong.db',
      securityLevel: relationalStore.SecurityLevel.S1
    })
    this.text = "创建数据库成功"
  }

  aboutToAppear() {
    this.getStore()
  }

  build() {
    Column({ space: 10 }) {

      Text(this.text)
        .fontSize(30)
      Button("创建数据库")
        .onClick(async () => {
          this.getStore()

        })

      Button("创建数据表")
        .onClick(async () => {
          await this.store?.executeSql(`
        CREATE TABLE IF NOT EXISTS ${this.tableName} (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          title TEXT NOT NULL,
          content TEXT NOT NULL,
          create_time INTEGER NOT NULL
        )
      `)
          this.text = "创建数据表格"
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

插入一条数据

Button("插入一条数据")
  .onClick(async () => {
    try {
    // 成功 返回 行id,失败返回 -1 
      const result = await  this.store?.insert(this.tableName, {
        title: "标题" + Date.now(),
        content: "内容" + Date.now(),
        create_time: Date.now()
      })

      this.text = `插入数据 ${result}`
    } catch (e) {
      promptAction.showToast({ message: `插入数据出错 ${e.message}` })

    }
  })

查询数据

Button("查询数据")
  .onClick(async () => {

    // 要查询哪个数据表格的数据
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    // predicates.equalTo("id", 4)
    // 要查询文章数据
    const resultSet = await  this.store?.query(predicates)
    const list: ArticleItem[] = []
    while (resultSet?.goToNextRow()) {
      list.push({
        id: resultSet.getLong(resultSet.getColumnIndex('id')),
        title: resultSet.getString(resultSet.getColumnIndex('title')),
        content: resultSet.getString(resultSet.getColumnIndex('content')),
        create_time: resultSet.getLong(resultSet.getColumnIndex('create_time'))
      })
    }
    // 关闭结果集  内存释放  后期无法 通过结果集再去拿数据 
    resultSet?.close()

    // this.list = list
    AlertDialog.show({ message: JSON.stringify(list, null, 2) })
  })

删除数据

Button("删除数据")
  .onClick(() => {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('id', 17)
    // 返回受影响的行数。

    const res = this.store?.deleteSync(predicates)
    this.text = "删除表格中的数据"
    promptAction.showToast({ message: `删除了${res}条数据` })
  })

修改数据

Button("修改数据")
  .onClick(() => {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    // predicates.equalTo('id', 18)
    const res = this.store?.updateSync({
      title: "标题修改的数据",
      // content: "内容修改的数据",
    }, predicates)
    promptAction.showToast({ message: `修改了${res}数据` })
  })

删除数据库

Button("删除数据库")
  .onClick(async () => {
    await relationalStore.deleteRdbStore(getContext(this), {
      name: 'interview_tong.db',
      securityLevel: relationalStore.SecurityLevel.S1
    })
    promptAction.showToast({ message: `删除数据库成功` })
  })

新建了录音数据库工具类

entry/src/main/ets/commons/utils/AudioDB.ets

import { relationalStore, ValuesBucket } from '@kit.ArkData'
import { ENTRY_ABILITY_CONTEXT } from '../constants'

/**
 * 指定录音数据类型
 */
export  interface InterviewAudioItem extends ValuesBucket {
  id: number | null
  // 用户的id 同一个APP 可以存不同的用户数据 user_id
  user_id: string
  // 录音文件的名字
  name: string
  // 录音存在沙箱目录下地址  播放录音要用到
  path: string
  // 时长
  duration: number
  // 大小
  size: number
  // 文件创建日期
  create_time: number
}

class AudioDB {
  store?: relationalStore.RdbStore
  tableName = 'interview_audio'

  // 初始化数据库
  async initStore() {
    const ctx = AppStorage.get<Context>(ENTRY_ABILITY_CONTEXT)
    if (ctx) {
      const store = await relationalStore.getRdbStore(ctx, {
        name: 'interview_audio.db',
        securityLevel: relationalStore.SecurityLevel.S1
      })
      const sql = `
        CREATE TABLE IF NOT EXISTS ${this.tableName} (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id TEXT NOT NULL,
          name TEXT NOT NULL,
          path TEXT NOT NULL,
          duration INTEGER NOT NULL,
          size INTEGER NOT NULL,
          create_time INTEGER NOT NULL
        )
      `
      await store.executeSql(sql)
      this.store = store
    }
  }

  // 添加
  async insert(item: InterviewAudioItem) {
    // 新插入的数据的id
    const rowId = await  this.store?.insert(this.tableName, item)
    if (rowId === undefined || rowId === -1) {
      return Promise.reject('insert fail')
    } else {
      return Promise.resolve()
    }
  }

  // 删除
  async delete(id: number) {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('id', id)
    const rowCount = await  this.store?.delete(predicates)
    if (rowCount === undefined || rowCount <= 0) {
      return Promise.reject('delete fail')
    } else {
      return Promise.resolve()
    }
  }

  // 修改
  async update(item: InterviewAudioItem) {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('id', item.id)
    const rowCount = await  this.store?.update(item, predicates)
    if (rowCount === undefined || rowCount <= 0) {
      return Promise.reject('update fail')
    } else {
      return Promise.resolve()
    }
  }

  // 根据用户 user_id 来查询所有录音数据
  async query(userId: string) {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('user_id', userId)
    const resultSet = await  this.store?.query(predicates)
    if (!resultSet) {
      return Promise.reject('query fail')
    }
    const list: InterviewAudioItem[] = []
    while (resultSet.goToNextRow()) {
      list.push({
        id: resultSet.getLong(resultSet.getColumnIndex('id')),
        user_id: resultSet.getString(resultSet.getColumnIndex('user_id')),
        name: resultSet.getString(resultSet.getColumnIndex('name')),
        path: resultSet.getString(resultSet.getColumnIndex('path')),
        duration: resultSet.getLong(resultSet.getColumnIndex('duration')),
        size: resultSet.getLong(resultSet.getColumnIndex('size')),
        create_time: resultSet.getLong(resultSet.getColumnIndex('create_time'))
      })
    }
    resultSet.close()
    return Promise.resolve(list)
  }
}

export  const audioDB = new AudioDB()


// interface A {
//   num: number
// }
//
//
// function aaa(a: A) {
//
// }
//
// interface B extends A {
//   num: number
//   name: string
// }
//
// const b: B = {
//   num: 100,
//   name: "xxx"
// }
// // 要求用 父类型, 而你传递子类型 有允许!!!
// aaa(b)

三,具体实现

录音 开始 结束 保存文件

entry/src/main/ets/views/Audio/AudioRecordComp.ets

// 是否正在录音
@State
isRecording: boolean = false
// 声纹振幅
@State
amplitude: number = 10
// 声音振幅定时器id
tId: number = -1
// 录音 实例
avRecorder?: media.AVRecorder
// 录音文件的沙箱路径  =  ctx.filesDir + `/1111.m4a`
filePath: string = ""
// 开始录音
startRecord = async () => {
  this.isRecording = true

//  开始录音 start

  // 指定录音文件的沙箱存储路径
  const ctx = getContext(this)
  // cacheDir  容量有限制 满了 会覆盖
  // filesDir 不卸载一直存在
  this.filePath = ctx.filesDir + `/${Date.now()}.m4a`

  // 获得文件权限
  const file = fileIo.openSync(this.filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)


  //   1 创建 avRecord
  this.avRecorder = await media.createAVRecorder();
  //   2 指定录音相关的配置信息
  await this.avRecorder.prepare({
    audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
    profile: {
      audioBitrate: 100000, // 音频比特率
      audioChannels: 2, // 音频声道数
      audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
      audioSampleRate: 48000, // 音频采样率
      fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
    },
    url: 'fd://' + file.fd,
  });

  //   3 开始录制
  await this.avRecorder.start()
  //  结束录音 end


  //   测试 设置  声纹振幅 大小变化
  this.tId = setInterval(async () => {
    // 500 - 20000 回去cv
    this.amplitude = await this.avRecorder!.getAudioCapturerMaxAmplitude()
    if (this.amplitude > 20000) {
      this.amplitude = 20000
    } else if (this.amplitude < 500) {
      this.amplitude = 500
    }
  }, 500)
}
// 结束录音
stopRecord = async () => {
  this.isRecording = false
clearInterval(this.tId)

  //   结束录音
  await this.avRecorder?.release()

  promptAction.showToast({ message: `保存录音成功` })

}

录音结束 存数据库 假数据

  1. 初始化数据库

  2. 结束录音 - 假数据 获取相关数据,存储到数据库中

       import { HcNavBar } from '../../commons/components/HcNavBar'
       import { audioDB } from '../../commons/utils/AudioDB'
       import { auth } from '../../commons/utils/Auth'
       import { InterviewAudioItem } from '../../models'

       import { AudioItemComp } from './AudioItemComp'
       import { AudioRecordComp } from './AudioRecordComp'
       import { promptAction } from '@kit.ArkUI'

       @Component
       export struct AudioView {
         @State list: InterviewAudioItem[] = [{} as InterviewAudioItem, {} as InterviewAudioItem]

         async aboutToAppear() {
           //   初始化数据库
           await audioDB.initStore()
           //   查询数据!!
           this.getList()
         }

         //   查询数据
         async getList() {
           this.list = await audioDB.query("xxxx")
         }

         // 新数据到数据库
         insert = async () => {

           //   插入数据
           const res = await audioDB.insert({
             id: null,
             user_id: "xxxx",
             // 存录音时 时间
             name: "xxxx",
             //   沙箱目录  子组件传递
             path: "xxxx",
             //   时长 子组件传递
             duration: 3333,
             //   文件的大小 新需求 新api 传递 沙箱目录给我 子组件传递
             size: 1000,
             //   文件创建日期  Date.now()
             create_time: 10000
           })

           promptAction.showToast({ message: `${res}` })
           this.getList()

         }

         build() {
           Column() {
             HcNavBar({ title: '面试录音', showRightIcon: false })
             Column() {
               Button("测试 新增数据")
                 .onClick(() => {
                   this.insert()
                 })
               List() {
                 ForEach(this.list, (item: InterviewAudioItem) => {
                   ListItem() {
                     AudioItemComp({
                       item
                     })
                   }
                 })
               }
               .width('100%')
               .height('100%')
             }
             .width('100%')
             .layoutWeight(1)

             AudioRecordComp()
           }
           .width('100%')
           .height('100%')
         }
       }

录音结束 存数据库 真数据

  1. 分析 子组件 AudioRecordComp.ets 要传递给父组件的数据 AudioView.ets

  1. 子组件传递数据

    1.   声明属性

    1.   记录开始录制的时间

    1.   构造数据数据传递给父组件

  1. 父组件接收数据

删除录音

  1. 实现列表左滑删除

  2. 删除数据库数据

  3. 删除沙箱文件

  4. 实现滑动删除的组件结构

  1. 结构

//   编辑和删除按钮
@Builder
itemBuilder(item: InterviewAudioItem) {
  Row() {
    Button("编辑")
      .layoutWeight(1)
      .backgroundColor(Color.Blue)
    Button("删除")
      .layoutWeight(1)
      .backgroundColor(Color.Red)
      .onClick(() => {
        this.onDeleteItem(item)
      })
  }
  .width("40%")
  .height("100%")
}
  1. 执行删除

日期时间处理 dayjs

Date

复杂

2024年9月30日 23:59:59 + 3秒 =

第三方日期处理库

  1. 安装

    1.   ohpm i dayjs
      
  2. 引入

    1.   import dayjs from 'dayjs'
      
  3. 插入数据的时候 使用

编辑录音的名称

新建 编辑对话框

对话框里面 放文本输入框-显示待编辑的录音的name

点击键盘 保存 通知父组件 执行 修改数据操作 - 数据库

         @CustomDialog
         export struct InputDialog {
           controller: CustomDialogController
           @Prop
           text: string = ""
           changeVoiceName: (t: string) => void = () => {
           }

           build() {
             Column() {
               TextInput({ text: $$this.text })
                 .onSubmit(() => {
                   //   通知父组件 录音名称修改了
                   this.changeVoiceName(this.text)
                 })
             }
             .width(200)
             .height(50)
             .backgroundColor(`rgba(0,0,0,0.6)`)
             .justifyContent(FlexAlign.Center)
             .borderRadius(25)
           }
         }

父组件声明相关的状态

点击 编辑 弹出对话框

* * *

父组件保存数据

播放录音

新建播放组件

  import { InterviewAudioItem } from '../../models'
  import { media } from '@kit.MediaKit'
  import { fileIo } from '@kit.CoreFileKit'
  import { promptAction } from '@kit.ArkUI'


  @Component
  export struct AudioPlayer {
    // 整个录音数据传递过来
    @Prop
    item: InterviewAudioItem
    // 播放器实例
    avPlayer: media.AVPlayer | undefined = undefined
  // 是否正在播放
    @State
    isPlaying: boolean = false
  // 总时长
    @State
    total: number = 0
    // 当前播放进度
    @State
    value: number = 0



    build() {
      Column() {
        //    图片
        Image($r('app.media.ic_mine_audio'))
          .width(100)
          .aspectRatio(1)
        //   名称
        Text(this.item.name)
          .fontSize(18)
        //   控制条
        Row({ space: 10 }) {
          Image(this.isPlaying ? $r('sys.media.ohos_ic_public_pause') : $r('sys.media.ohos_ic_public_play'))
            .width(24)
            .aspectRatio(1)
            .onClick(() => {
              if (this.isPlaying) {
                //   想要暂停
                // this.pause()
              } else {
                //   想要播放
                // this.play()

              }
            })

          //   进度条组件
          Progress({ total: this.total, value: this.value })
            .layoutWeight(1)

        }
        .width("90%")
      }
      .width("100%")
      .height("100%")
      .justifyContent(FlexAlign.Center)

      .backgroundColor(Color.White)
    }
  }

父组件通过全模态来显示播放页面

播放页面 开始自动播放

播放页面实现 暂停

播放页面实现 恢复播放

播放页面实现 进度条功能

* * *

退出自动销毁