在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用

news2025/6/12 19:54:59

1. 项目配置与权限设置

1.1 配置module.json5

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "录音需要麦克风权限"
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "reason": "保存录音文件到媒体库"
      },
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "读取录音文件"
      }
    ],
    "abilities": [
      {
        "name": "EntryAbility",
        "backgroundModes": ["audioRecording"]
      }
    ]
  }
}

2. 录音核心功能实现

2.1 录音服务封装

// src/main/ets/service/AudioRecorder.ts
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';
import mediaLibrary from '@ohos.multimedia.mediaLibrary';

export class AudioRecorder {
  private audioRecorder: audio.AudioRecorder | null = null;
  private filePath: string = '';
  private audioOptions: audio.AudioRecorderOptions = {
    encoder: audio.AudioEncoder.AAC_LC,
    sampleRate: audio.AudioSampleRate.SAMPLE_RATE_44100,
    numberOfChannels: audio.AudioChannel.CHANNEL_2,
    format: audio.AudioOutputFormat.MPEG_4,
    uri: '', // 将在startRecording时设置
    location: { latitude: 0, longitude: 0 } // 可选地理位置
  };

  async startRecording(): Promise<boolean> {
    try {
      // 创建录音保存路径
      const context = getContext(this) as Context;
      const dir = context.filesDir + '/recordings';
      await fs.ensureDir(dir);
      this.filePath = `${dir}/recording_${new Date().getTime()}.m4a`;
      
      // 初始化录音器
      this.audioRecorder = await audio.createAudioRecorder();
      this.audioOptions.uri = `file://${this.filePath}`;
      
      // 配置并启动录音
      await this.audioRecorder.prepare(this.audioOptions);
      await this.audioRecorder.start();
      return true;
    } catch (err) {
      console.error('启动录音失败:', err);
      return false;
    }
  }

  async stopRecording(): Promise<string | null> {
    if (!this.audioRecorder) return null;
    
    try {
      await this.audioRecorder.stop();
      await this.audioRecorder.release();
      this.audioRecorder = null;
      
      // 保存到媒体库
      const media = mediaLibrary.getMediaLibrary(getContext(this) as Context);
      const fileAsset = await media.createAsset(
        mediaLibrary.MediaType.AUDIO,
        'recording.m4a',
        this.filePath
      );
      
      return fileAsset.uri;
    } catch (err) {
      console.error('停止录音失败:', err);
      return null;
    }
  }

  async pauseRecording(): Promise<boolean> {
    if (!this.audioRecorder) return false;
    
    try {
      await this.audioRecorder.pause();
      return true;
    } catch (err) {
      console.error('暂停录音失败:', err);
      return false;
    }
  }

  async resumeRecording(): Promise<boolean> {
    if (!this.audioRecorder) return false;
    
    try {
      await this.audioRecorder.resume();
      return true;
    } catch (err) {
      console.error('恢复录音失败:', err);
      return false;
    }
  }

  getRecordingState(): audio.AudioState | null {
    return this.audioRecorder?.getState() || null;
  }
}

2.2 录音状态管理

// src/main/ets/model/RecordingModel.ts
export class RecordingModel {
  @Tracked recordingState: 'idle' | 'recording' | 'paused' = 'idle';
  @Tracked currentDuration: number = 0; // 毫秒
  @Tracked filePath: string | null = null;
  private timer: number | null = null;
  
  startTimer() {
    this.stopTimer();
    this.timer = setInterval(() => {
      this.currentDuration += 1000;
    }, 1000);
  }
  
  stopTimer() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }
  
  reset() {
    this.stopTimer();
    this.recordingState = 'idle';
    this.currentDuration = 0;
    this.filePath = null;
  }
}

3. 用户界面实现

3.1 主录音界面

// src/main/ets/pages/RecordPage.ets
import { AudioRecorder } from '../service/AudioRecorder';
import { RecordingModel } from '../model/RecordingModel';

@Entry
@Component
struct RecordPage {
  private audioRecorder: AudioRecorder = new AudioRecorder();
  @State recordingModel: RecordingModel = new RecordingModel();
  
  build() {
    Column() {
      // 录音时间显示
      Text(this.formatTime(this.recordingModel.currentDuration))
        .fontSize(40)
        .margin({ top: 50 })
      
      // 波形图占位
      this.buildWaveform()
      
      // 录音控制按钮
      this.buildControlButtons()
      
      // 录音文件列表
      if (this.recordingModel.filePath) {
        this.buildRecordingInfo()
      }
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  buildWaveform() {
    // 实现波形图显示
    Canvas(this.context)
      .width('90%')
      .height(150)
      .margin({ top: 30, bottom: 30 })
      .onReady(() => {
        // 绘制波形图的逻辑
      })
  }
  
  @Builder
  buildControlButtons() {
    Row() {
      if (this.recordingModel.recordingState === 'idle') {
        Button('开始录音', { type: ButtonType.Circle })
          .width(80)
          .height(80)
          .backgroundColor('#FF5722')
          .onClick(() => this.startRecording())
      } else {
        if (this.recordingModel.recordingState === 'recording') {
          Button('暂停', { type: ButtonType.Circle })
            .width(60)
            .height(60)
            .backgroundColor('#4CAF50')
            .onClick(() => this.pauseRecording())
        } else {
          Button('继续', { type: ButtonType.Circle })
            .width(60)
            .height(60)
            .backgroundColor('#2196F3')
            .onClick(() => this.resumeRecording())
        }
        
        Button('停止', { type: ButtonType.Circle })
          .width(60)
          .height(60)
          .backgroundColor('#F44336')
          .margin({ left: 30 })
          .onClick(() => this.stopRecording())
      }
    }
    .margin({ top: 50 })
  }
  
  @Builder
  buildRecordingInfo() {
    Column() {
      Text('录音文件:')
        .fontSize(16)
        .margin({ bottom: 10 })
      
      Text(this.recordingModel.filePath || '')
        .fontSize(14)
        .fontColor('#666666')
      
      Button('播放录音')
        .margin({ top: 20 })
        .onClick(() => this.playRecording())
    }
    .margin({ top: 30 })
  }
  
  async startRecording() {
    const success = await this.audioRecorder.startRecording();
    if (success) {
      this.recordingModel.recordingState = 'recording';
      this.recordingModel.startTimer();
    }
  }
  
  async pauseRecording() {
    const success = await this.audioRecorder.pauseRecording();
    if (success) {
      this.recordingModel.recordingState = 'paused';
      this.recordingModel.stopTimer();
    }
  }
  
  async resumeRecording() {
    const success = await this.audioRecorder.resumeRecording();
    if (success) {
      this.recordingModel.recordingState = 'recording';
      this.recordingModel.startTimer();
    }
  }
  
  async stopRecording() {
    const filePath = await this.audioRecorder.stopRecording();
    if (filePath) {
      this.recordingModel.filePath = filePath;
    }
    this.recordingModel.reset();
  }
  
  async playRecording() {
    // 实现播放录音功能
  }
  
  formatTime(milliseconds: number): string {
    const totalSeconds = Math.floor(milliseconds / 1000);
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  }
}

3.2 录音列表页面

// src/main/ets/pages/RecordingsListPage.ets
import { mediaLibrary } from '@ohos.multimedia.mediaLibrary';

@Entry
@Component
struct RecordingsListPage {
  @State recordings: Array<mediaLibrary.FileAsset> = [];
  
  aboutToAppear() {
    this.loadRecordings();
  }
  
  async loadRecordings() {
    try {
      const media = mediaLibrary.getMediaLibrary(getContext(this) as Context);
      const fetchOpts: mediaLibrary.MediaFetchOptions = {
        selections: `${mediaLibrary.FileKey.MEDIA_TYPE}=?`,
        selectionArgs: [mediaLibrary.MediaType.AUDIO.toString()],
        order: `${mediaLibrary.FileKey.DATE_ADDED} DESC`
      };
      
      const fetchResult = await media.getFileAssets(fetchOpts);
      this.recordings = await fetchResult.getAllObject();
    } catch (err) {
      console.error('加载录音列表失败:', err);
    }
  }
  
  build() {
    List({ space: 10 }) {
      ForEach(this.recordings, (recording) => {
        ListItem() {
          RecordingItem({ recording: recording })
        }
      })
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct RecordingItem {
  private recording: mediaLibrary.FileAsset;
  
  build() {
    Row() {
      Image($r('app.media.ic_audio'))
        .width(40)
        .height(40)
        .margin({ right: 15 })
      
      Column() {
        Text(this.recording.displayName)
          .fontSize(16)
        
        Text(this.formatDate(this.recording.dateAdded * 1000))
          .fontSize(12)
          .fontColor('#888888')
      }
      .layoutWeight(1)
      
      Text(this.formatDuration(this.recording.duration))
        .fontSize(14)
    }
    .padding(15)
  }
  
  formatDate(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
  }
  
  formatDuration(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  }
}

4. 音频播放功能实现

4.1 音频播放器封装

// src/main/ets/service/AudioPlayer.ts
import audio from '@ohos.multimedia.audio';

export class AudioPlayer {
  private audioPlayer: audio.AudioPlayer | null = null;
  private currentUri: string = '';
  
  async play(uri: string): Promise<boolean> {
    if (this.audioPlayer && this.currentUri === uri) {
      await this.audioPlayer.play();
      return true;
    }
    
    try {
      await this.stop();
      
      this.audioPlayer = await audio.createAudioPlayer();
      this.currentUri = uri;
      
      await this.audioPlayer.reset();
      await this.audioPlayer.setSource({ source: uri });
      await this.audioPlayer.play();
      
      return true;
    } catch (err) {
      console.error('播放失败:', err);
      return false;
    }
  }
  
  async pause(): Promise<boolean> {
    if (!this.audioPlayer) return false;
    
    try {
      await this.audioPlayer.pause();
      return true;
    } catch (err) {
      console.error('暂停失败:', err);
      return false;
    }
  }
  
  async stop(): Promise<boolean> {
    if (!this.audioPlayer) return true;
    
    try {
      await this.audioPlayer.stop();
      await this.audioPlayer.release();
      this.audioPlayer = null;
      this.currentUri = '';
      return true;
    } catch (err) {
      console.error('停止失败:', err);
      return false;
    }
  }
  
  async seek(position: number): Promise<boolean> {
    if (!this.audioPlayer) return false;
    
    try {
      await this.audioPlayer.seek(position);
      return true;
    } catch (err) {
      console.error('跳转失败:', err);
      return false;
    }
  }
}

4.2 播放控制组件

// src/main/ets/components/PlayerControls.ets
@Component
export struct PlayerControls {
  private player: AudioPlayer;
  @State isPlaying: boolean = false;
  @State currentPosition: number = 0;
  @State duration: number = 0;
  private updateInterval: number | null = null;
  
  build() {
    Column() {
      // 进度条
      Slider({
        value: this.currentPosition,
        min: 0,
        max: this.duration,
        step: 1,
        style: SliderStyle.OutSet
      })
      .onChange((value: number) => {
        this.player.seek(value);
      })
      
      // 时间显示
      Row() {
        Text(this.formatTime(this.currentPosition))
          .fontSize(12)
        
        Blank()
        
        Text(this.formatTime(this.duration))
          .fontSize(12)
      }
      .width('100%')
      
      // 控制按钮
      Row() {
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => {
            if (this.isPlaying) {
              this.player.pause();
            } else {
              this.player.play();
            }
          })
        
        Button('停止')
          .margin({ left: 20 })
          .onClick(() => {
            this.player.stop();
          })
      }
      .margin({ top: 15 })
    }
  }
  
  aboutToAppear() {
    this.startUpdatingPosition();
  }
  
  aboutToDisappear() {
    this.stopUpdatingPosition();
  }
  
  startUpdatingPosition() {
    this.updateInterval = setInterval(() => {
      // 更新当前播放位置
    }, 500);
  }
  
  stopUpdatingPosition() {
    if (this.updateInterval) {
      clearInterval(this.updateInterval);
      this.updateInterval = null;
    }
  }
  
  formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  }
}

5. 功能扩展建议

  1. ​录音质量设置​​:

    • 添加不同采样率和编码格式选项
    • 实现高质量和低质量录音切换
  2. ​录音编辑功能​​:

    • 实现录音裁剪功能
    • 添加淡入淡出效果
  3. ​录音标签管理​​:

    • 为录音添加标签和备注
    • 实现录音分类管理
  4. ​云同步功能​​:

    • 集成华为云存储
    • 实现录音多设备同步
  5. ​高级音频处理​​:

    • 添加降噪功能
    • 实现音频变速播放

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2407520.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)

UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中&#xff0c;UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化&#xf…

SpringCloudGateway 自定义局部过滤器

场景&#xff1a; 将所有请求转化为同一路径请求&#xff08;方便穿网配置&#xff09;在请求头内标识原来路径&#xff0c;然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…

(转)什么是DockerCompose?它有什么作用?

一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用&#xff0c;而无需手动一个个创建和运行容器。 Compose文件是一个文本文件&#xff0c;通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …

多种风格导航菜单 HTML 实现(附源码)

下面我将为您展示 6 种不同风格的导航菜单实现&#xff0c;每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…

用docker来安装部署freeswitch记录

今天刚才测试一个callcenter的项目&#xff0c;所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…

多模态大语言模型arxiv论文略读(108)

CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题&#xff1a;CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者&#xff1a;Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…

QT: `long long` 类型转换为 `QString` 2025.6.5

在 Qt 中&#xff0c;将 long long 类型转换为 QString 可以通过以下两种常用方法实现&#xff1a; 方法 1&#xff1a;使用 QString::number() 直接调用 QString 的静态方法 number()&#xff0c;将数值转换为字符串&#xff1a; long long value 1234567890123456789LL; …

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建

华为云FlexusDeepSeek征文&#xff5c;DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色&#xff0c;华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型&#xff0c;能助力我们轻松驾驭 DeepSeek-V3/R1&#xff0c;本文中将分享如何…

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心

当仓库学会“思考”&#xff0c;物流的终极形态正在诞生 想象这样的场景&#xff1a; 凌晨3点&#xff0c;某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径&#xff1b;AI视觉系统在0.1秒内扫描包裹信息&#xff1b;数字孪生平台正模拟次日峰值流量压力…

自然语言处理——循环神经网络

自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元&#xff08;GRU&#xff09;长短期记忆神经网络&#xff08;LSTM&#xff09…

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子&#xff0c;再用 CNN-BiLSTM-Attention 来动态预测每个子序列&#xff0c;最后重构出总位移&#xff0c;预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵&#xff08;S…

select、poll、epoll 与 Reactor 模式

在高并发网络编程领域&#xff0c;高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表&#xff0c;以及基于它们实现的 Reactor 模式&#xff0c;为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。​ 一、I…

SpringTask-03.入门案例

一.入门案例 启动类&#xff1a; package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…

在WSL2的Ubuntu镜像中安装Docker

Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包&#xff1a; for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…

第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词

Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵&#xff0c;其中每行&#xff0c;每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid&#xff0c;其中有多少个 3 3 的 “幻方” 子矩阵&am…

【JavaWeb】Docker项目部署

引言 之前学习了Linux操作系统的常见命令&#xff0c;在Linux上安装软件&#xff0c;以及如何在Linux上部署一个单体项目&#xff0c;大多数同学都会有相同的感受&#xff0c;那就是麻烦。 核心体现在三点&#xff1a; 命令太多了&#xff0c;记不住 软件安装包名字复杂&…

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…

ArcGIS Pro制作水平横向图例+多级标注

今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作&#xff1a;ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等&#xff08;ArcGIS出图图例8大技巧&#xff09;&#xff0c;那这次我们看看ArcGIS Pro如何更加快捷的操作。…

网络编程(UDP编程)

思维导图 UDP基础编程&#xff08;单播&#xff09; 1.流程图 服务器&#xff1a;短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…