一、前言

语音相关能力在鸿蒙项目里十分常见,语音笔记、客服语音消息、语音转文字、音频回放上传等业务都离不开音频模块。很多开发在调试时,前台录音播放一切正常,一旦退到后台、频繁启停音频,就会遇到各种问题:后台录音中断、再次打开提示音频占用、上传后资源没释放导致内存持续上涨,甚至直接触发应用闪退。

这类问题大多不是 API 调用错误,而是生命周期和资源管理没有处理到位。本文基于最新鸿蒙 API,讲解音频录制、播放、后台保活、文件上传完整流程,所有代码可直接复制运行。

二、权限前置配置

音频能力需要在module.json5配置静态权限,同时代码中动态申请敏感权限。

// module.json5
"requestPermissions": [
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:mic_permission_reason",
    "usedScene": {
      "abilities": ["./EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
    "reason": "$string:bg_permission_reason",
    "usedScene": {
      "abilities": ["./EntryAbility"],
      "when": "inuse"
    }
  }
]

说明:MICROPHONE麦克风权限属于敏感权限,必须动态申请;后台持续录音,需要申请后台运行权限,上架时权限说明要写清楚业务用途。

三、音频录制实现(支持后台录音)

使用 AudioCapturer 完成音频采集,录制的音频保存到应用沙箱目录。后台录音核心要点:启动录音前申请后台任务,防止应用切后台被系统挂起。

import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
import { backgroundTaskManager } from '@kit.BackgroundTaskKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct AudioRecordDemo {
  private audioCapturer: audio.AudioCapturer | null = null;
  private fileFd: number = -1;
  private bgTaskId: number = -1;
  @State isRecording: boolean = false;
  private filePath: string = '';

  // 初始化音频采集器
  async initCapturer() {
    const capturerInfo: audio.AudioCapturerInfo = {
      source: audio.SourceType.SOURCE_TYPE_MIC,
      streamInfo: {
        samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
        channels: audio.AudioChannel.CHANNEL_1,
        sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE
      }
    };
    this.audioCapturer = await audio.createAudioCapturer(capturerInfo);
  }

  // 开始录音,申请后台任务
  async startRecord() {
    if (this.isRecording) return;
    await this.initCapturer();
    const context = getContext(this);
    this.filePath = `${context.filesDir}/record_audio.pcm`;
    this.fileFd = await fileIo.open(this.filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);

    // 申请后台长时任务,保障后台录音
    this.bgTaskId = await backgroundTaskManager.startBackgroundRunning(context,
      backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK);

    await this.audioCapturer?.start();
    this.isRecording = true;
    this.readAudioData();
  }

  // 循环读取音频数据流写入文件
  async readAudioData() {
    if (!this.isRecording || !this.audioCapturer) return;
    const buffer = await this.audioCapturer.read();
    if (buffer.length > 0 && this.fileFd > 0) {
      await fileIo.write(this.fileFd, buffer);
    }
    setTimeout(() => this.readAudioData(), 10);
  }

  // 停止录音,释放资源
  async stopRecord() {
    if (!this.isRecording || !this.audioCapturer) return;
    await this.audioCapturer.stop();
    this.isRecording = false;
    await fileIo.close(this.fileFd);
    // 结束后台任务
    if (this.bgTaskId !== -1) {
      const ctx = getContext(this);
      backgroundTaskManager.stopBackgroundRunning(ctx, this.bgTaskId);
      this.bgTaskId = -1;
    }
    // 释放音频实例,避免占用
    this.audioCapturer.release();
    this.audioCapturer = null;
    promptAction.showToast({message: "录音已保存"});
  }

  build() {
    Column() {
      Row() {
        Button(this.isRecording ? "停止录音" : "开始录音")
          .onClick(() => {
            if(this.isRecording) {
              this.stopRecord()
            } else {
              this.startRecord()
            }
          })
      }
      .margin(20)
    }
    .width('100%')
  }
}

四、音频播放实现

AudioRenderer 用来播放沙箱内录制好的 pcm 音频文件,用完必须 release 释放实例。

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

@Component
struct AudioPlayDemo {
  private audioRenderer: audio.AudioRenderer | null = null;
  private fileFd: number = -1;

  async initRenderer() {
    const renderInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
      streamInfo: {
        samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
        channels: audio.AudioChannel.CHANNEL_1,
        sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE
      }
    }
    this.audioRenderer = await audio.createAudioRenderer(renderInfo);
  }

  async startPlay(filePath: string) {
    await this.initRenderer();
    this.fileFd = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
    await this.audioRenderer?.start();
    this.playLoop();
  }

  async playLoop() {
    if (!this.audioRenderer) return;
    const buf = new ArrayBuffer(4096);
    const readLen = await fileIo.read(this.fileFd, buf);
    if(readLen > 0) {
      await this.audioRenderer.write(buf);
      this.playLoop();
    } else {
      await this.stopPlay();
    }
  }

  async stopPlay() {
    if(!this.audioRenderer) return;
    await this.audioRenderer.stop();
    await fileIo.close(this.fileFd);
    this.audioRenderer.release();
    this.audioRenderer = null;
  }

  build() {
    Button("播放录音")
      .onClick(async () => {
        const ctx = getContext(this);
        await this.startPlay(`${ctx.filesDir}/record_audio.pcm`)
      })
  }
}

五、音频文件上传

录音完成后,读取沙箱音频文件,通过网络模块上传到后端。

import { http } from '@kit.NetworkKit';
import { fileIo } from '@kit.CoreFileKit';

async function uploadAudio(filePath: string) {
  const fileFd = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
  const stat = await fileIo.stat(filePath);
  const buf = new ArrayBuffer(stat.size as number);
  await fileIo.read(fileFd, buf);

  const httpReq: http.HttpRequestOptions = {
    method: http.RequestMethod.POST,
    extraData: buf,
    header: {
      "Content-Type": "application/octet-stream"
    }
  }
  const res = await http.request("https://xxx/upload", httpReq);
  console.info("上传结果", res.result);
  await fileIo.close(fileFd);
}

六、资源释放避坑重点

音频硬件属于系统独占资源,这也是最容易踩坑的地方。

  1. 实例释放:AudioCapturer、AudioRenderer 使用结束,一定要调用 release,不能单纯 stop。只 stop 不 release,音频硬件会被持续占用,下次启动录音直接报错。
  2. 文件句柄:打开的 fd 必须 close。反复录制不关闭句柄,会耗尽应用文件描述符,触发 IO 异常。
  3. 后台任务配对:startBackgroundRunning 之后,必须成对调用 stopBackgroundRunning。如果应用异常退出,系统会自动回收,但正常业务流程一定要手动结束后台任务,否则会影响应用功耗评分。
  4. 页面销毁:页面销毁时,如果录音 / 播放还在进行,要主动停止并释放所有实例。页面生命周期 onDisappear 里增加资源回收逻辑。
  5. 并发限制:不要同时创建多个 AudioCapturer 实例,同一时刻只允许一个录音实例。

七、小结

音频模块开发,难点不在于采集和播放本身,而是资源管理和后台运行的配合。录音启动时申请后台任务保证后台持续采集,结束时依次停止采集、关闭文件句柄、释放音频实例、关闭后台任务。

录音文件上传完成之后,可根据业务需求选择保留或删除本地 pcm 文件,减少存储空间占用。把资源释放逻辑封装到统一工具类,在页面销毁、录音中断等场景统一调用,就能规避绝大多数音频占用、内存泄漏类问题。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐