声音怎么录下来?HarmonyOS音频录音与文件管理的app,我来教你如何开发

1 阅读3分钟

如果你对声音训练感兴趣,可以去鸿蒙应用市场搜一下**「声研坊」**,下载下来体验体验。录制自己的声音,回放对比,分析发声技巧。体验完了再回来看这篇文章,你会更清楚录音功能是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,Web端的录音API(MediaRecorder)功能有限。去年开始转战鸿蒙生态,用ArkTS开发App,发现HarmonyOS的音频录制API功能更强大。

比如:

  • 录音控制:Web端用MediaRecorder.start()/stop();鸿蒙里用AudioCapturer更精细的控制。
  • 音频格式:Web端格式支持有限;鸿蒙里可以指定采样率、位深、声道数。
  • 文件保存:Web端用Blob生成URL;鸿蒙里直接写入文件系统。

别担心,接下来这篇文章,我会用"声研坊"的录音功能,带你看看HarmonyOS怎么实现音频录制。


这篇文章聊什么

声研坊的录音功能,核心要解决:

  1. 录音控制:开始、暂停、停止录音
  2. 音频配置:采样率、格式等参数设置
  3. 文件保存:录音保存到应用目录
  4. 录音管理:查看和播放录音文件

第一步:设计录音数据结构

// Web前端同学看这里:Web端用Blob存储录音数据
// 鸿蒙里用文件路径引用录音文件

// 录音记录
interface Recording {
  id: string;
  title: string;
  filePath: string;
  duration: number;        // 时长(秒)
  fileSize: number;        // 文件大小(字节)
  sampleRate: number;      // 采样率
  channels: number;        // 声道数
  createdAt: string;
}

// 录音配置
interface AudioConfig {
  sampleRate: number;      // 采样率: 8000, 16000, 44100, 48000
  channels: number;        // 声道数: 1(单声道), 2(立体声)
  bitDepth: number;        // 位深: 8, 16, 24
  format: string;          // 格式: 'wav', 'aac', 'mp3'
}

// 预设配置
const AUDIO_PRESETS: AudioConfig[] = [
  { sampleRate: 16000, channels: 1, bitDepth: 16, format: 'wav' },
  { sampleRate: 44100, channels: 1, bitDepth: 16, format: 'wav' },
  { sampleRate: 44100, channels: 2, bitDepth: 16, format: 'wav' },
  { sampleRate: 48000, channels: 2, bitDepth: 24, format: 'wav' },
];

第二步:实现录音功能

// Web前端同学看这里:Web端用navigator.mediaDevices.getUserMedia()获取音频流
// 鸿蒙里用audio.AudioCapturer创建录音器

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

let audioCapturer: audio.AudioCapturer | null = null;
let isRecording: boolean = false;
let recordedFile: string = '';

// 创建录音器
async function createAudioCapturer(config: AudioConfig): Promise<void> {
  const capturerOptions: audio.AudioCapturerOptions = {
    streamInfo: {
      samplingRate: config.sampleRate as audio.AudioSamplingRate,
      channels: config.channels as audio.AudioChannel,
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
    },
    capturerInfo: {
      source: audio.SourceType.SOURCE_TYPE_MIC,
      capturerFlags: 0
    }
  };

  audioCapturer = await audio.createAudioCapturer(capturerOptions);
}

// 开始录音
async function startRecording(context: Context, config: AudioConfig): Promise<boolean> {
  try {
    await createAudioCapturer(config);

    // 准备输出文件
    const fileName = `recording_${Date.now()}.wav`;
    recordedFile = `${context.filesDir}/${fileName}`;

    await audioCapturer!.start();
    isRecording = true;

    // 读取音频数据并写入文件
    const bufferSize = 4096;
    const file = fileIo.openSync(recordedFile, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);

    // 注意:实际实现需要持续读取音频数据
    // 这里简化处理,实际应使用循环读取
    audioCapturer!.read(bufferSize, true).then((buffer: ArrayBuffer) => {
      fileIo.writeSync(file.fd, buffer);
    });

    return true;
  } catch (err) {
    console.error(`开始录音失败: ${err}`);
    return false;
  }
}

// 停止录音
async function stopRecording(): Promise<string | null> {
  try {
    if (audioCapturer) {
      await audioCapturer.stop();
      await audioCapturer.release();
      audioCapturer = null;
    }
    isRecording = false;
    return recordedFile;
  } catch (err) {
    console.error(`停止录音失败: ${err}`);
    return null;
  }
}

// 释放资源
function releaseCapturer(): void {
  if (audioCapturer) {
    audioCapturer.release();
    audioCapturer = null;
  }
}

第三步:实现录音页面

// Web前端同学看这里:React里我们用state管理录音状态
// 鸿蒙里用@State装饰器,结合定时器更新录音时长

@Entry
@Component
struct RecordingPage {
  @State isRecording: boolean = false
  @State isPaused: boolean = false
  @State duration: number = 0
  @State selectedPreset: number = 0
  @State recordings: Recording[] = []

  private timer: number = 0

  async aboutToAppear() {
    this.recordings = await getRecordings(getContext(this) as Context);
  }

  async startRecording() {
    const config = AUDIO_PRESETS[this.selectedPreset];
    const success = await startRecording(getContext(this) as Context, config);

    if (success) {
      this.isRecording = true;
      this.isPaused = false;
      this.duration = 0;

      // 开始计时
      this.timer = setInterval(() => {
        if (!this.isPaused) {
          this.duration++;
        }
      }, 1000);
    }
  }

  async stopRecording() {
    const filePath = await stopRecording();

    if (filePath) {
      clearInterval(this.timer);
      this.isRecording = false;

      // 保存录音记录
      const recording: Recording = {
        id: `rec_${Date.now()}`,
        title: `录音 ${this.recordings.length + 1}`,
        filePath,
        duration: this.duration,
        fileSize: 0, // 实际应读取文件大小
        sampleRate: AUDIO_PRESETS[this.selectedPreset].sampleRate,
        channels: AUDIO_PRESETS[this.selectedPreset].channels,
        createdAt: new Date().toISOString().slice(0, 10)
      };

      await saveRecording(getContext(this) as Context, recording);
      this.recordings = await getRecordings(getContext(this) as Context);
    }
  }

  formatDuration(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }

  build() {
    Column() {
      Text('录音')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      // 录音配置选择
      Text('录音质量')
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 8 })

      Row() {
        ForEach(AUDIO_PRESETS, (preset: AudioConfig, index: number) => {
          Button(`${preset.sampleRate / 1000}kHz`)
            .fontSize(12)
            .height(32)
            .backgroundColor(this.selectedPreset === index ? '#3b82f6' : '#f3f4f6')
            .fontColor(this.selectedPreset === index ? '#ffffff' : '#374151')
            .borderRadius(16)
            .margin({ right: 8 })
            .onClick(() => { this.selectedPreset = index })
        })
      }
      .margin({ bottom: 20 })

      // 录音时长显示
      Text(this.formatDuration(this.duration))
        .fontSize(48)
        .fontWeight(FontWeight.Light)
        .fontColor(this.isRecording ? '#ef4444' : '#374151')
        .margin({ bottom: 20 })

      // 录音按钮
      Button(this.isRecording ? '停止' : '录音')
        .width(100)
        .height(100)
        .backgroundColor(this.isRecording ? '#ef4444' : '#3b82f6')
        .borderRadius(50)
        .fontSize(18)
        .onClick(() => {
          if (this.isRecording) {
            this.stopRecording();
          } else {
            this.startRecording();
          }
        })

      // 录音列表
      Text('录音记录')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 30, bottom: 12 })

      ForEach(this.recordings, (recording: Recording) => {
        Row() {
          Column() {
            Text(recording.title)
              .fontSize(14)
              .fontWeight(FontWeight.Medium)

            Text(`${this.formatDuration(recording.duration)} · ${recording.createdAt}`)
              .fontSize(12)
              .fontColor('#6b7280')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Button('播放')
            .fontSize(12)
            .height(32)
            .backgroundColor('#22c55e')
            .borderRadius(8)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#ffffff')
        .borderRadius(12)
        .margin({ bottom: 8 })
      })
    }
    .padding(16)
  }
}

第四步:常见问题

4.1 录音权限

问题:调用录音API报错。

解决:在module.json5里声明麦克风权限ohos.permission.MICROPHONE

4.2 录音文件格式

问题:不同设备支持的音频格式不同。

解决:使用WAV格式,兼容性最好,或者查询设备支持的格式。


总结

这篇文章围绕"声研坊"的录音功能,讲解了:

音频录制

  • AudioCapturer的创建和配置
  • 录音控制(开始、停止)
  • 音频参数设置

文件管理

  • 录音文件保存
  • 录音记录存储
  • 录音列表展示

如果你对"声研坊"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。