鸿蒙开发-开发了一个鸿蒙原生的录音机

312 阅读2分钟

1. 获取权限

1.1 封装了获取权限的工具类

import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit'

class PermissionPlugin {
  // 检查单个权限
  checkAccessToken(permission: Permissions) {
    try {
      const atManager = abilityAccessCtrl.createAtManager();
      const bundleInfo =
        bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
      const accessTokenId = bundleInfo.appInfo.accessTokenId
      const grantStatus = atManager.checkAccessTokenSync(accessTokenId, permission);
      return grantStatus;
    } catch (e) {
      return abilityAccessCtrl.GrantStatus.PERMISSION_DENIED
    }
  }

  // 检查多个权限
  async checkPermissions(permissions: Permissions[]) {
    for (let permission of permissions) {
      const grantStatus = this.checkAccessToken(permission)
      if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
        return false
      }
    }
    return true
  }

  // 首次向用户申请授权
  async requestPermissions(permissions: Permissions[]) {
    const atManager = abilityAccessCtrl.createAtManager();
    const res = await atManager.requestPermissionsFromUser(getContext(), permissions) // 首次
    const isAuth = res.authResults.every(item => item === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    return isAuth ? Promise.resolve() : Promise.reject()
  }

  // 二次向用户申请授权
  async requestPermissionOnSetting(permissions: Permissions[]) {
    const atManager = abilityAccessCtrl.createAtManager();
    const grantStatus = await atManager.requestPermissionOnSetting(getContext(), permissions) // 二次
    const isAuth = grantStatus.every(item => item === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    return isAuth ? Promise.resolve() : Promise.reject()
  }
}

// 暴露实例对象
export const permissionPlugin = new PermissionPlugin()

1.2 申请录音权限

src/main/module.json5

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

1.3 调用方法获取权限

aboutToAppear() {
  // 首次获取权限
  permissionPlugin.requestPermissions(["ohos.permission.MICROPHONE"])
    .catch(() => {
      // 用户拒绝 去设置页继续申请
      permissionPlugin.requestPermissionOnSetting(['ohos.permission.MICROPHONE'])
    })
}

运行效果

PixPin_2024-11-19_20-39-01.gif

2. 实现录音计时功能

2.1 定义播放状态和时间戳

@State recordIng: boolean = false // 是否正在播放
@State timeStamp: number = 0 // 录音时间戳

2.2 检查权限

const grantStatus = permissionPlugin.checkAccessToken("ohos.permission.MICROPHONE")

2.3 开始录音并计时

aboutToAppear(): void {
  const grantStatus = permissionPlugin.checkAccessToken("ohos.permission.MICROPHONE")
  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
    // 申请权限
    permissionPlugin.requestPermissionOnSetting(["ohos.permission.MICROPHONE"])
  } else {
    // 开始录音
    this.beginRecord()
  }
}

beginRecord() {
  this.recordIng = true
  setInterval(() => {
    this.timeStamp += 10
  }, 10)
}

getDisplayTime 函数用于将时间戳转换为显示时间格式(分钟:秒)。

~~(this.timeStamp / 60 / 1000):计算分钟数,并向下取整。

(this.timeStamp / 1000 % 60).toFixed(2):计算秒数,并保留两位小数。

padStart(2, "0") 和 padStart(5, "0"):确保分钟和秒数分别至少有两位和五位数字,不足部分用0补齐。

getDisplayTime() {
  return `${(~~(this.timeStamp / 60 / 1000)).toString().padStart(2, "0")}: ${(this.timeStamp / 1000 % 60).toFixed(2)
    .padStart(5, "0")}`
}

运行结果

PixPin_2024-11-19_21-09-45.gif

3. 封装音频录制管理类

1.定义类 AudioRecordManager

定义了一个名为 AudioRecordManager 的类,用于管理音频录制的整个过程。类中包含一个可选属性 capturer,表示音频捕获器的实例。

2.创建音频捕获器实例

定义了一个异步方法 createCapturer,用于创建 AudioCapturer 实例。这个方法的主要步骤如下:

1.初始化音频流的信息,包括采样率、通道、采样格式和编码格式。

2.初始化音频捕获器的信息,包括音频来源和捕获器标志。

3.使用上述信息创建 AudioCapturer 实例,并将其赋值给 capturer 属性。

3.开始录音

定义了一个异步方法 start,用于开始录音。这个方法的主要步骤如下:

1.检查 capturer 是否已创建,如果没有则调用 createCapturer 方法创建。

2.监听 readData 事件,当有录音数据时,调用传入的回调函数处理数据。

3.调用 capturer 的 start 方法开始录音。

4.结束录音

定义了一个异步方法 stop,用于结束录音。这个方法的主要步骤如下:

1.检查 capturer 是否存在,如果存在则调用其 stop 方法停止录音。

2.释放 capturer 的资源,并将 capturer 属性设为 undefined,以释放内存。

5.创建单例

创建了一个 AudioRecordManager 类的单例 audioRecordManager,以便在整个应用中使用。

import { audio } from '@kit.AudioKit';

/**
 * 音频录制管理器,负责管理音频的录制过程
 */
export class AudioRecordManager {
  // 录音引擎的单例
  capturer?: audio.AudioCapturer

  /**
   * 创建AudioCapturer实例
   * 这个方法是异步的,因为它需要初始化音频流和捕获器设置,并创建AudioCapturer实例
   */
  async createCapturer() {
    // 音频流的信息,包括采样率、通道、采样格式和编码格式
    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 audioCapturerInfo: audio.AudioCapturerInfo = {
      // 音频来源,这里设置为麦克风
      source: audio.SourceType.SOURCE_TYPE_MIC,
      // 捕获器标志,这里设置为0
      capturerFlags: 0
    };

    // 音频捕获器的选项,包含音频流和捕获器信息
    let audioCapturerOptions: audio.AudioCapturerOptions = {
      // 音频流信息
      streamInfo: audioStreamInfo,
      // 捕获器信息
      capturerInfo: audioCapturerInfo
    };
    // 创建AudioCapturer实例对象
    this.capturer = await audio.createAudioCapturer(audioCapturerOptions);
  }

  /**
   * 开始录音
   * @param callBack 当录音数据可用时调用的回调函数,它接收一个ArrayBuffer作为参数
   * 这个方法是异步的,如果capturer尚未创建,需要先创建capturer
   */
  async start(callBack: (bf: ArrayBuffer) => void) {
    // 如果capturer不存在,则创建一个新的capturer实例
    if (!this.capturer) {
      await this.createCapturer()
    }
    // 监听"readData"事件,当有录音数据时,调用回调函数处理数据
    this.capturer?.on("readData", (buffer) => {
      callBack(buffer)
    })
    // 开始录音
    this.capturer?.start()
  }

  /**
   * 结束录音
   * 这个方法是异步的,负责停止录音并释放资源
   */
  async stop() {
    // 如果capturer存在,则停止录音并释放资源
    if (this.capturer) {
      await this.capturer.stop()
      this.capturer.release() // 释放资源
      this.capturer = undefined // 这行代码值五十块钱 释放内存 因为已经结束任务
    }
  }
}

// 音频录制管理器的单例
export const audioRecordManager = new AudioRecordManager()

改造

1.创建沙箱文件

  • getContext().filesDir:获取应用的私有文件目录路径,这个目录通常位于应用的沙箱环境中。

  • newFileName:包含 .mp3 扩展名的文件名。

  • fileIo.openSync:同步打开或创建文件。

  • fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE:指定文件打开模式,

CREATE 表示如果文件不存在则创建, READ_WRITE 表示以读写模式打开文件。

// 为文件名添加.mp3扩展名
const newFileName = fileName + ".mp3"

// 打开或创建文件,准备写入录音数据
const file =
  fileIo.openSync(getContext().filesDir + '/' + newFileName, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)

2.根据偏移量写入buffer

  • file.fd:文件描述符,表示已打开的文件。

  • buffer:要写入的录音数据,类型为 ArrayBuffer。

  • offset:文件中的当前偏移量,表示从文件的哪个位置开始写入数据。

  • length:要写入的数据长度,即 buffer 的字节长度。

// 监听录音数据事件,当有数据时调用回调函数并写入文件
this.capturer?.on("readData", (buffer) => {
  if (buffer) {
    callBack(buffer)
    // 写入文件
    fileIo.writeSync(file.fd, buffer, {
      offset: offset,
      length: buffer.byteLength
    })
    offset += buffer.byteLength
  }
})

3.如果文件存在,则在原文件的基础上继续写入

// 获取文件初始偏移量,用于后续写入数据
let offset = fileIo.statSync(file.fd).size

完整代码

/**
 * 异步启动录音功能,并将录音数据回调和保存到文件中
 * 
 * @param callBack 回调函数,当有录音数据可用时调用,接收一个ArrayBuffer类型的录音数据
 * @param fileName 要保存的文件名,不包含扩展名
 */
async start(callBack: (bf: ArrayBuffer) => void, fileName: string) {
  // 如果capturer尚未创建,则进行创建
  if (!this.capturer) {
    await this.createCapturer()
  }
  
  // 为文件名添加.mp3扩展名
  const newFileName = fileName + ".mp3"
  
  // 打开或创建文件,准备写入录音数据
  const file =
    fileIo.openSync(getContext().filesDir + '/' + newFileName, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
  
  // 获取文件初始偏移量,用于后续写入数据
  let offset = fileIo.statSync(file.fd).size
  
  // 监听录音数据事件,当有数据时调用回调函数并写入文件
  this.capturer?.on("readData", (buffer) => {
    if (buffer) {
      callBack(buffer)
      // 写入文件
      fileIo.writeSync(file.fd, buffer, {
        offset: offset,
        length: buffer.byteLength
      })
      offset += buffer.byteLength
    }
  })
  
  // 开始录音
  this.capturer?.start()
}

4.实现录音功能

4.1 定义文件名称和定时器id

fileName: string = "" // 文件名称 
timer: number = -1 // 定时器id

4.2 改造开始录音方法

/**
 * 开始录音记录
 * 此函数初始化录音过程,包括启动定时器记录时间戳和开始录音文件的录制
 */
beginRecord() {
  this.recordIng = true // 标记录音状态为正在录音
  this.timer = setInterval(() => {
    this.timeStamp += 10 // 每10毫秒增加一次时间戳
  }, 10)
  if (!this.fileName) {
    this.fileName = Date.now().toString() // 如果未指定文件名,则使用当前时间戳作为文件名
  }
  audioRecordManager.start((bf) => {
    // 开始录音的回调函数,此处未执行任何操作
  }, this.fileName) // 开始录音
}

4.3 暂停录音逻辑实现

/**
 * 暂停录音记录
 * 此函数停止录音过程,包括停止录音输入和清除计时器
 */
async pauseRecord() {
  audioRecordManager.stop() // 停止录音输入
  this.recordIng = false // 标记录音状态为未在录音
  clearInterval(this.timer) // 清除定时器
}

4.4 注册点击事件

Row() {
  Image(this.recordIng ? $r("sys.media.ohos_ic_public_pause") : $r("sys.media.ohos_ic_public_play"))
    .width(24)
    .aspectRatio(1)
}
.onClick(() => {
  if (this.recordIng) {
    this.pauseRecord()
  } else {
    this.beginRecord()
  }
})

4.5 运行结果展示(到这里已经实现了录音功能)

PixPin_2024-11-20_14-41-03.gif

应用沙箱中生成一个MP3文件 image.png

5.渲染列表

获取数据

// 保存录音数据
async saveRecord() {
  let file = fileIo.statSync(this.filePath)
  let data: InterviewAudioItem = {
    id: this.startTime,
    name: this.filePath.split('/').pop()!,
    path: this.filePath,
    duration: this.endTime - this.startTime,
    size: file.size
  }
  const pre = preferences.getPreferencesSync(getContext(), { name: 'recordAudioList' })
  pre.putSync(data.id.toString(), JSON.stringify(data))
  await pre.flush()
}

// 获取录音列表
async getRecordAudioList() {
  const pre = preferences.getPreferencesSync(getContext(), { name: 'recordAudioList' })
  let objlist = pre.getAllSync()
  let list = Object.values(objlist) as string[]
  let recordAudioList = list.map(value => {
    return JSON.parse(value) as InterviewAudioItem
  })
  return recordAudioList
}
// 获取沙箱中的文件
onPageShow(): void {
  this.getPlayList()
}

/**
 * 获取播放列表
 * 该方法用于扫描应用文件目录下的所有.mp3文件,并将它们的信息加载到列表中
 * 这有助于创建一个包含所有可用音频文件的播放列表
 */
async getPlayList() {
  this.list = await audioRecordManager.getRecordAudioList()
}

引入dayjs

ohpm install dayjs

渲染列表

List({ space: 20 }) {
  ForEach(this.list, (item: InterviewAudioItem) => {
    ListItem() {
      Column({ space: 10 }) {
        Row({ space: 10 }) {
          Column({ space: 10 }) {
            Text(item.name).fontSize(16).fontWeight(600)
            Text(dayjs(item.id).format("YYYY-MM-DD HH:mm:ss")).fontSize(14).fontColor('#999999')
          }.alignItems(HorizontalAlign.Start)

          Image(this.playName === item.name && this.playIng ?
          $r("sys.media.ohos_ic_public_pause")
            : $r("sys.media.ohos_ic_public_play"))
            .width(24)
            .aspectRatio(1)
          
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        if (this.isShow && item.name === this.playName) {
          Column() {
            Slider({
              min: 0,
              max: this.max,
              value: this.value
            })
              .onChange((value) => {
                this.value = value
              })
            Row() {
              Text('0')
                .fontSize(14)
                .fontColor('#999999')
              Text(dayjs(this.max).format("HH:mm:ss"))
                .fontSize(14)
                .fontColor('#999999')
            }
            .padding(10)
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
          }
        }
      }
      .padding(10)
      .backgroundColor(Color.White)
      .borderRadius(15)
    }
  })
}
.margin({ top: 50 })
.padding({ left: 20, right: 20, top: 20 })

image.png

6.实现音频播放

6.1 音频渲染管理器类

import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';

/**
 * 音频渲染管理器类,负责管理音频的播放流程
 */
export class AudioRenderManager {
  // 音频渲染器实例,初始为null
  render: audio.AudioRenderer | null = null

  /**
   * 初始化音频渲染器
   */
  async init() {
    // 音频流信息配置
    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_VOICE_COMMUNICATION,
      rendererFlags: 0
    };

    // 音频渲染器选项配置
    let audioRendererOptions: audio.AudioRendererOptions = {
      streamInfo: audioStreamInfo,
      rendererInfo: audioRendererInfo
    };
    // 创建音频渲染器
    this.render = await audio.createAudioRenderer(audioRendererOptions)
  }

  /**
   * 开始播放音频
   * @param fileName 音频文件名
   * @param callback 播放完成后的回调函数
   */
  async start(fileName: string, callback: () => void) {
    // 如果渲染器未初始化,则进行初始化
    if (!this.render) {
      await this.init()
    }
    let offset = 0
    // 同步打开音频文件
    const file = fileIo.openSync(getContext().filesDir + "/" + fileName)
    const statFile = fileIo.statSync(file.fd)
    // 监听渲染器的writeData事件,以读取和播放音频数据
    this.render?.on("writeData", (bf: ArrayBuffer) => {
      fileIo.readSync(file.fd, bf, {
        offset: offset,
        length: bf.byteLength
      })
      offset += bf.byteLength
      // 如果达到文件末尾,则调用回调函数并停止播放
      if (offset >= statFile.size) {
        callback()
        this.stop()
      }
    })
    // 开始播放音频
    this.render?.start()
  }

  /**
   * 暂停音频播放
   */
  async pause() {
    if (this.render) {
      await this.render.pause()
    }
  }

  /**
   * 停止音频播放,并释放资源
   */
  async stop() {
    if (this.render) {
      await this.render.stop()
      await this.render.release()
      this.render = null
    }
  }
}

// 音频渲染管理器的单例实例
export const audioRenderManager = new AudioRenderManager()

6.2 设置点击事件

Image(this.playName === item.name && this.playIng ?
$r("sys.media.ohos_ic_public_pause")
  : $r("sys.media.ohos_ic_public_play"))
  .width(24)
  .aspectRatio(1)
  .onClick(async () => {
    // 判断当前是否正在播放
    if (this.playName === item.name) {
      // 一个是 暂停 一个是播放
      if (this.playIng) {
        await audioRenderManager.stop()
        // 这里不能有后续逻辑了!!!
      } else {
        audioRenderManager.start(item.path.split('/').pop()!, () => {
          // 结束时触发
          this.playIng = false
        })
      }
      this.playIng = !this.playIng
    } else {
      // 第一次点击任何一个播放
      this.isShow = true
      this.max = item.duration
      this.playName = item.name // 记录当前播放的名称
      // 如果正在播放 一定是其他的在播放
      await audioRenderManager.stop()
      this.playIng = true // 这里无论如何得设置一下 万一别人没播呢?
      audioRenderManager.start(item.path.split('/').pop()!, () => {
        // 结束时触发
        this.playIng = false
      })
    }
  })

PixPin_2024-11-22_11-14-52.gif

7 实现删除功能

// 删除录音
async delRecordAudioList(id: number) {
  const pre = preferences.getPreferencesSync(getContext(), { name: 'recordAudioList' })
  let str = pre.getSync(id.toString(), '') as string
  let obj = JSON.parse(str) as InterviewAudioItem
  fileIo.unlinkSync(obj.path) // 删除文件
  pre.deleteSync(id.toString())
  await pre.flush()
}
.swipeAction({
  end: this.getListItemEnd(item.id)
})
@Builder
getListItemEnd(id: number) {
  Column() {
    Text("删除")
      .fontSize(12)
      .fontColor(Color.White)
      .fontWeight(FontWeight.Bold)
  }
  .margin(10)
  .justifyContent(FlexAlign.Center)
  .height(40)
  .width(40)
  .borderRadius(20)
  .backgroundColor(Color.Red)
  .onClick(async () => {
    // 弹窗确认
    promptAction.showDialog({
      title: '提示',
      message: '确定删除吗?',
      buttons: [
        {
          text: '确定',
          color: '#f40',
        },
        {
          text: '取消',
          color: '#999999',
        }
      ],
    }).then(async (value) => {
      if (value.index === 0) {
        audioRecordManager.delRecordAudioList(id)
        this.list = await audioRecordManager.getRecordAudioList()
      }
    })
  })
}

PixPin_2024-11-22_11-16-54.gif

8 保存录音

8.1封装一个弹窗实现返回时确定文件名称

import { promptAction } from '@kit.ArkUI'

@CustomDialog
@Component
export struct FileNameDialog {
  controller: CustomDialogController
  @State
  fileName: string = ""
  onConfirm: (name: string) => void = () => {
  }
  onCancel: () => void = () => {
  }

  build() {
    Column({ space: 20 }) {
      Column({ space: 20 }) {
        Text("保存当前音频文件为手机音频?")

        TextInput({
          text: $$this.fileName,
          placeholder: '请输入音频名称'
        })
          .height(36)
      }
      .padding({
        top: 20
      })
      .layoutWeight(1)

      Row() {
        Text("删除")
          .fontColor(Color.White)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .backgroundColor(Color.Red)
          .height("100%")
          .borderRadius(10)
          .onClick(() => {
            this.onCancel() // 李姐
          })
        Text("确定")
          .fontColor(Color.White)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .backgroundColor(Color.Blue)
          .height("100%")
          .borderRadius(10)
          .onClick(() => {
            if (this.fileName) {
              this.onConfirm(this.fileName)
            } else {
              promptAction.showToast({ message: '录音名称不能为空' })
            }

          })
      }
      .height(40)

    }
    .width(280)
    .height(200)
    .backgroundColor(Color.White)
    .borderRadius(10)
    .padding(10)
  }
}

8.2 修改录音名字

// 修改录音名字
async rename(id: number, name: string) {
  const pre = preferences.getPreferencesSync(getContext(), { name: 'recordAudioList' })
  let str = pre.getSync(id.toString(), '') as string
  let obj = JSON.parse(str) as InterviewAudioItem
  let newPath = obj.path.replace(obj.name, name)
  fileIo.renameSync(obj.path, newPath)
  obj.name = name
  obj.path = newPath
  pre.putSync(id.toString(), JSON.stringify(obj))
}

8.3 弹窗

dialog: CustomDialogController = new CustomDialogController({
  builder: FileNameDialog({
    fileName: this.fileName,
    onConfirm: async (name: string) => {
      if ((await audioRecordManager.getRecordAudioList()).some(item => item.name === name)) {
        // 如果存在同名文件,则提示并返回
        promptAction.showToast({
          message: "文件名已存在",
          duration: 2000
        })
        return
      }
      audioRecordManager.rename(audioRecordManager.startTime, name)
      this.dialog.close()
      router.back()
    },
    onCancel: () => {
      audioRecordManager.delRecordAudioList(audioRecordManager.startTime)
      this.dialog.close()
      router.back()
    }
  }),
  alignment: DialogAlignment.Center,
  customStyle: true
})
Row() {
  Row() {
    Row()
      .width(18)
      .aspectRatio(1)
      .borderRadius(4)
      .backgroundColor(Color.White)
  }
  .justifyContent(FlexAlign.Center)
  .width(50)
  .aspectRatio(1)
  .backgroundColor(Color.Red)
  .borderRadius(25)
  .onClick(() => {
    // 停止录音
    this.pauseRecord()
    this.dialog.open()
  })

PixPin_2024-11-22_12-00-33.gif

9 实现波形计算

9.1封装波形音频组件

@Component
export struct AudioWave {
  controller: WaveController = new WaveController()
  @State
  list: number[] = []

  aboutToAppear(): void {
    this.initController()
  }

  initController() {
    this.controller.transBuffer = (bf: ArrayBuffer) => {
      this.calculateAmplitudes(bf)
    }
  }

  calculateAmplitudes(buffer: ArrayBuffer) {
    try {
      const sampleSize = Math.floor(buffer.byteLength / 80); // 计算每个均分区间的大小
      const view = new DataView(buffer);
      const amplitudes: number[] = [];
      // 遍历缓冲区,计算每个样本的振幅并转换为高度
      // 遍历原始缓冲区,提取每个均分区间的振幅
      for (let i = 0; i < buffer.byteLength; i += sampleSize) {
        let sum = 0;
        // 计算当前均分区间内所有振幅的平均值
        for (let j = i; j < i + sampleSize && j < buffer.byteLength; j += 2) { // 假设每个样本占据 2 字节
          const sample = view.getInt16(j, true);
          sum += Math.abs(sample);
        }
        const averageAmplitude = 500 * sum / (sampleSize / 2) / 32767; // 假设每个样本占据 2 字节
        amplitudes.push(averageAmplitude < 10 ? 10 : averageAmplitude);
      }
      animateTo({ duration: 100 }, () => {
        this.list = amplitudes
      })

    } catch (error) {
      AlertDialog.show({ message: error.message })
      throw new Error(error.message)
    }
  }

  build() {
    Row({ space: 2 }) {
      ForEach(this.list, (val: number) => {
        Row()
          .width(2)
          .height(val)
          .backgroundColor("#bababa")
      })
    }
    .width("100%")
    .height(240)
    .backgroundColor("#f6f6f6")
    .justifyContent(FlexAlign.Center)
  }
}

export class WaveController {
  transBuffer: (bf: ArrayBuffer) => void = () => {
  }
}
controller: WaveController = new WaveController()


  // 底部音轨区
  AudioWave({
    controller: this.controller
  })
}
.width("100%")
.height(400)
.backgroundColor(Color.White)
.borderRadius(10)

image.png

PixPin_2024-11-22_14-25-48.gif

仓库地址

/SoundRecorder (gitee.com)