HarmonyOS音频编解码开发实战

举报
Jack20 发表于 2026/06/20 20:50:04 2026/06/20
【摘要】 HarmonyOS开发中的音频编解码:@ohos.multimedia.media、CodecBase、AAC/MP3/FLAC 编解码实战核心要点:音频编解码是音频开发的核心基础设施。本文从 @ohos.multimedia.media 的编解码 API 入手,深入讲解 CodecBase 基础框架、AAC/MP3/FLAC 三种主流格式的编解码实现、硬件编解码的选择策略,以及编解码性能...

HarmonyOS开发中的音频编解码:@ohos.multimedia.media、CodecBase、AAC/MP3/FLAC 编解码实战

核心要点:音频编解码是音频开发的核心基础设施。本文从 @ohos.multimedia.media 的编解码 API 入手,深入讲解 CodecBase 基础框架、AAC/MP3/FLAC 三种主流格式的编解码实现、硬件编解码的选择策略,以及编解码性能优化的实战技巧。


一、背景与动机

你有没有想过,为什么一首 5 分钟的无损 WAV 歌曲动辄 50MB,而同样一首 AAC 格式的歌只要 5MB,听起来却差不多?

这就是音频编解码的魔力——用巧妙的数学算法,把音频数据压缩到原来的十分之一甚至更小,同时尽量保持听觉上的"无感"。

在鸿蒙开发中,音频编解码无处不在:录音需要编码保存、播放需要解码渲染、语音通话需要实时编解码、音乐制作需要无损处理……如果你不理解编解码的原理和 API 用法,就很难做出高质量的音频应用。

更关键的是,鸿蒙支持硬件编解码软件编解码两种模式。选错了,轻则性能浪费,重则发热卡顿。怎么选?这就是本文要回答的问题。


二、核心原理

2.1 音频编解码的基本概念

音频编码(Encoding)是将原始 PCM 音频数据压缩为特定格式的过程,解码(Decoding)则是逆过程。

flowchart LR
    A[原始音频 PCM] --> B[编码器 Encoder]
    B --> C[压缩数据<br/>AAC/MP3/FLAC]
    C --> D[存储/传输]
    D --> E[解码器 Decoder]
    E --> F[还原音频 PCM]
    F --> G[音频渲染播放]

    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    class A,F info
    class B,E primary
    class C warning
    class D purple
    class G info

2.2 三种主流格式对比

特性 AAC MP3 FLAC
压缩类型 有损 有损 无损
典型码率 128-256 kbps 128-320 kbps 700-1100 kbps
压缩比 ~10:1 ~10:1 ~2:1
音质 同码率优于 MP3 经典格式 完美还原
编码延迟 低(~20ms) 中(~50ms) 低(~10ms)
适用场景 流媒体、直播 兼容性播放 音乐制作、HiFi
鸿蒙支持 ✅ 硬编硬解 ✅ 软解为主 ✅ 软解为主

2.3 CodecBase 基础框架

鸿蒙的编解码框架基于 CodecBase 抽象层,提供了统一的编解码接口:

// CodecBase 核心概念
interface CodecBase {
  // 编解码器类型
  mimeType: string;        // 如 'audio/aac', 'audio/mpeg', 'audio/flac'
  // 是否为硬件编解码器
  isHardware: boolean;
  // 编解码能力查询
  name: string;
}

2.4 硬编解码 vs 软编解码

flowchart TD
    A[编解码选择] --> B{需要实时处理?}
    B -->|| C{设备支持硬编?}
    B -->|| D[软编解码<br/>兼容性好]

    C -->|| E[硬编解码<br/>低延迟低功耗]
    C -->|| F[软编解码<br/>降级方案]

    A --> G{需要无损?}
    G -->|| H[FLAC 软编解码]
    G -->|| I{追求压缩率?}
    I -->|| J[AAC 硬编解码]
    I -->|| K[MP3 软编解码]

    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    class A info
    class E,J primary
    class D,F,K warning
    class H purple

硬件编解码利用设备 DSP/NPU 专用硬件单元,功耗低、延迟小,但格式支持有限。软件编解码使用 CPU 计算,兼容性好但功耗高、延迟大。


三、代码实战

3.1 基础:使用 AVMuxer + AVCodec 进行 AAC 编码

下面的示例展示了如何将 PCM 音频数据编码为 AAC 格式。

// 文件名:AacEncoder.ets
// 功能:PCM → AAC 编码器

import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct AacEncoderPage {
  @State encodeStatus: string = '就绪';
  @State encodeProgress: number = 0;
  @State outputInfo: string = '';

  // AAC 编码配置
  private aacConfig: media.AVCodecConfig = {
    audioConfig: {
      sampleRate: 44100,              // 采样率 44.1kHz
      channelCount: 2,                // 双声道
      bitRate: 128000,                // 码率 128kbps
      sampleFormat: media.AudioSampleFormat.SAMPLE_F32LE,  // 32位浮点
    },
    mimeType: 'audio/aac',
  };

  aboutToAppear() {
    // 查询设备支持的编码能力
    this.queryEncoderCapabilities();
  }

  // 查询编码能力
  async queryEncoderCapabilities() {
    try {
      // 获取所有音频编码器
      const codecs = await media.getAudioEncoderCaps();
      console.info('[Encoder] 支持的音频编码器:');
      for (const codec of codecs) {
        console.info(`  - ${codec.name} (${codec.mimeType}) 硬件: ${codec.isHardware}`);
      }
    } catch (error) {
      console.error(`[Encoder] 查询编码能力失败: ${JSON.stringify(error)}`);
    }
  }

  // 执行 AAC 编码
  async encodeToAac() {
    this.encodeStatus = '编码中...';
    this.encodeProgress = 0;

    let encoder: media.AVCodec | null = null;
    let muxer: media.AVMuxer | null = null;
    let outputFile: fileIo.File | null = null;

    try {
      // 1. 创建 AAC 编码器
      encoder = await media.createAudioEncoderByName('avcodec_aac_encoder');
      console.info('[Encoder] AAC 编码器创建成功');

      // 2. 配置编码器
      await encoder.configure(this.aacConfig);
      console.info('[Encoder] 编码器配置完成');

      // 3. 准备输出文件
      const context = getContext(this) as common.UIAbilityContext;
      const outputPath = context.filesDir + '/output_aac.mp4';
      outputFile = fileIo.openSync(outputPath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);

      // 4. 创建封装器(Muxer)——将编码后的 AAC 数据封装为 MP4 容器
      muxer = await media.createAVMuxer(outputFile.fd, media.ContainerFormatType.CFT_MPEG_4A);
      console.info('[Encoder] 封装器创建成功');

      // 5. 注册编码回调
      encoder.on('outputBuffer', (index: number, buffer: media.BufferInfo, flags: media.BufferFlag) => {
        // 获取编码后的 AAC 数据
        const encodedData = encoder?.getOutputBuffer(index);
        if (encodedData && buffer.size > 0) {
          // 将编码数据写入封装器
          muxer?.writeSampleData(0, encodedData, buffer);
          console.info(`[Encoder] 编码输出: ${buffer.size} 字节, presentationTimeUs: ${buffer.presentationTimeUs}`);
        }
        // 释放输出缓冲区
        encoder?.releaseOutputBuffer(index);
      });

      encoder.on('inputBuffer', (index: number) => {
        // 填充 PCM 数据到输入缓冲区
        this.fillPcmData(encoder!, index);
      });

      // 6. 启动编码
      await encoder.start();
      console.info('[Encoder] 编码启动');

      // 模拟编码过程
      for (let i = 0; i < 100; i++) {
        this.encodeProgress = i;
        await this.sleep(20);
      }

      // 7. 停止编码
      await encoder.stop();
      await encoder.release();
      encoder = null;

      // 8. 停止封装器
      await muxer?.stop();
      await muxer?.release();
      muxer = null;

      this.encodeStatus = '编码完成';
      this.encodeProgress = 100;
      this.outputInfo = `输出文件: ${outputPath}`;

    } catch (error) {
      console.error(`[Encoder] 编码失败: ${JSON.stringify(error)}`);
      this.encodeStatus = `编码失败: ${error.message}`;
    } finally {
      // 清理资源
      if (encoder) {
        await encoder.release().catch(() => {});
      }
      if (muxer) {
        await muxer.release().catch(() => {});
      }
      if (outputFile) {
        fileIo.closeSync(outputFile);
      }
    }
  }

  // 填充 PCM 数据(模拟)
  private fillPcmData(encoder: media.AVCodec, index: number) {
    const inputBuffer = encoder.getInputBuffer(index);
    if (!inputBuffer) return;

    // 实际开发中,这里应该从录音缓冲区或文件读取真实的 PCM 数据
    // 此处填充静音数据作为示例
    const frameSize = 1024 * 2 * 4; // 1024 采样 × 2 声道 × 4 字节(float32)
    const silenceData = new ArrayBuffer(frameSize);
    inputBuffer.writeSync(silenceData, 0, frameSize, 0);

    const bufferInfo: media.BufferInfo = {
      presentationTimeUs: Date.now() * 1000,
      size: frameSize,
      offset: 0,
      flags: 0,
    };

    encoder.queueInputBuffer(index, bufferInfo);
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  build() {
    Column() {
      Text('AAC 音频编码器')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .margin({ bottom: 20 })

      // 编码配置信息
      Column() {
        Text('编码配置').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#4FC3F7')
        Text('格式: AAC | 采样率: 44100Hz | 声道: 立体声 | 码率: 128kbps')
          .fontSize(13)
          .fontColor('#AAAAAA')
          .margin({ top: 8 })
      }
      .width('100%')
      .padding(15)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .margin({ bottom: 15 })

      // 编码状态
      Row() {
        Text('状态:')
          .fontSize(14)
          .fontColor('#888888')
        Text(this.encodeStatus)
          .fontSize(14)
          .fontColor(this.encodeStatus === '编码完成' ? '#81C784' :
                     this.encodeStatus.includes('失败') ? '#EF5350' : '#4FC3F7')
          .margin({ left: 8 })
      }
      .margin({ bottom: 15 })

      // 进度条
      Progress({ value: this.encodeProgress, total: 100, type: ProgressType.Linear })
        .width('100%')
        .color('#6C63FF')
        .margin({ bottom: 15 })

      // 输出信息
      if (this.outputInfo) {
        Text(this.outputInfo)
          .fontSize(12)
          .fontColor('#81C784')
          .margin({ bottom: 15 })
      }

      // 开始编码按钮
      Button('开始编码')
        .onClick(() => this.encodeToAac())
        .width('100%')
        .height(50)
        .backgroundColor('#6C63FF')
        .borderRadius(12)
        .enabled(this.encodeStatus !== '编码中...')
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#0d0d1a')
  }
}

3.2 进阶:MP3/FLAC 解码播放

解码是编码的逆过程。下面的示例展示了如何使用 AVCodec 解码 MP3 和 FLAC 文件。

// 文件名:AudioDecoder.ets
// 功能:MP3/FLAC 音频解码器,支持格式自动检测

import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 解码格式枚举
enum DecodeFormat {
  AAC = 'audio/aac',
  MP3 = 'audio/mpeg',
  FLAC = 'audio/flac',
  AUTO = 'auto',
}

// 解码器状态
interface DecoderState {
  format: string;
  sampleRate: number;
  channelCount: number;
  bitRate: number;
  duration: number;
  isDecoding: boolean;
  decodedFrames: number;
}

@Entry
@Component
struct AudioDecoderPage {
  @State decoderState: DecoderState = {
    format: '未选择',
    sampleRate: 0,
    channelCount: 0,
    bitRate: 0,
    duration: 0,
    isDecoding: false,
    decodedFrames: 0,
  };
  @State selectedFormat: string = 'auto';

  private decoder: media.AVCodec | null = null;
  private audioRenderer: media.AudioRenderer | null = null;

  // 根据文件扩展名检测格式
  private detectFormat(fileName: string): string {
    const ext = fileName.split('.').pop()?.toLowerCase();
    switch (ext) {
      case 'mp3':
        return 'audio/mpeg';
      case 'flac':
        return 'audio/flac';
      case 'aac':
      case 'm4a':
        return 'audio/aac';
      case 'wav':
        return 'audio/wav';
      default:
        return 'audio/mpeg'; // 默认按 MP3 处理
    }
  }

  // 创建解码器
  async createDecoder(mimeType: string): Promise<media.AVCodec> {
    // 查询支持的解码器
    const decoderCaps = await media.getAudioDecoderCaps();
    console.info('[Decoder] 支持的音频解码器:');
    for (const cap of decoderCaps) {
      console.info(`  - ${cap.name} (${cap.mimeType}) 硬件: ${cap.isHardware}`);
    }

    // 优先选择硬件解码器
    let decoderName = '';
    for (const cap of decoderCaps) {
      if (cap.mimeType === mimeType && cap.isHardware) {
        decoderName = cap.name;
        console.info(`[Decoder] 选择硬件解码器: ${decoderName}`);
        break;
      }
    }

    // 如果没有硬件解码器,选择软件解码器
    if (!decoderName) {
      for (const cap of decoderCaps) {
        if (cap.mimeType === mimeType) {
          decoderName = cap.name;
          console.info(`[Decoder] 选择软件解码器: ${decoderName}`);
          break;
        }
      }
    }

    if (!decoderName) {
      throw new Error(`不支持的格式: ${mimeType}`);
    }

    return await media.createAudioDecoderByName(decoderName);
  }

  // 执行解码
  async startDecode() {
    this.decoderState.isDecoding = true;
    this.decoderState.decodedFrames = 0;

    try {
      // 1. 确定解码格式
      const mimeType = this.selectedFormat === 'auto'
        ? 'audio/mpeg'  // 实际应从文件检测
        : this.selectedFormat;

      // 2. 创建解码器
      this.decoder = await this.createDecoder(mimeType);
      this.decoderState.format = mimeType;

      // 3. 配置解码器
      const decodeConfig: media.AVCodecConfig = {
        audioConfig: {
          sampleRate: 44100,
          channelCount: 2,
          bitRate: 128000,
          sampleFormat: media.AudioSampleFormat.SAMPLE_S16LE,
        },
        mimeType: mimeType,
      };
      await this.decoder.configure(decodeConfig);

      // 4. 注册解码回调
      this.decoder.on('outputBuffer', (index: number, buffer: media.BufferInfo, flags: media.BufferFlag) => {
        this.decoderState.decodedFrames++;

        // 获取解码后的 PCM 数据
        const pcmData = this.decoder?.getOutputBuffer(index);
        if (pcmData && buffer.size > 0) {
          // 将 PCM 数据送入音频渲染器播放
          this.renderPcmData(pcmData, buffer.size);

          // 更新解码状态信息
          this.decoderState.sampleRate = 44100;
          this.decoderState.channelCount = 2;
        }

        // 释放输出缓冲区
        this.decoder?.releaseOutputBuffer(index);
      });

      this.decoder.on('inputBuffer', (index: number) => {
        // 从文件读取编码数据并送入解码器
        this.feedEncodedData(this.decoder!, index);
      });

      // 5. 启动解码
      await this.decoder.start();
      console.info('[Decoder] 解码启动');

    } catch (error) {
      console.error(`[Decoder] 解码失败: ${JSON.stringify(error)}`);
      this.decoderState.isDecoding = false;
    }
  }

  // 喂入编码数据(模拟)
  private feedEncodedData(decoder: media.AVCodec, index: number) {
    const inputBuffer = decoder.getInputBuffer(index);
    if (!inputBuffer) return;

    // 实际开发中,从文件或网络读取编码数据
    // 此处模拟一小段 MP3 帧数据
    const frameSize = 4096;
    const mockData = new ArrayBuffer(frameSize);
    inputBuffer.writeSync(mockData, 0, frameSize, 0);

    const bufferInfo: media.BufferInfo = {
      presentationTimeUs: Date.now() * 1000,
      size: frameSize,
      offset: 0,
      flags: 0,
    };

    decoder.queueInputBuffer(index, bufferInfo);
  }

  // 渲染 PCM 数据
  private async renderPcmData(data: ArrayBuffer, size: number) {
    // 实际开发中,将解码后的 PCM 数据送入 AudioRenderer 播放
    // 此处仅打印日志
    if (this.decoderState.decodedFrames % 100 === 0) {
      console.info(`[Decoder] 已解码 ${this.decoderState.decodedFrames} 帧, 当前帧大小: ${size}`);
    }
  }

  // 停止解码
  async stopDecode() {
    if (this.decoder) {
      await this.decoder.stop();
      await this.decoder.release();
      this.decoder = null;
    }
    this.decoderState.isDecoding = false;
  }

  build() {
    Scroll() {
      Column() {
        Text('音频解码器')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')
          .margin({ bottom: 20 })

        // 格式选择
        Text('选择解码格式').fontSize(14).fontColor('#888888').margin({ bottom: 8 })
        Row() {
          ForEach(['auto', 'audio/mpeg', 'audio/flac', 'audio/aac'], (format: string) => {
            Button(format === 'auto' ? '自动检测' : format.split('/')[1].toUpperCase())
              .onClick(() => { this.selectedFormat = format; })
              .fontSize(12)
              .height(36)
              .backgroundColor(this.selectedFormat === format ? '#6C63FF' : '#16213e')
              .fontColor(this.selectedFormat === format ? '#FFFFFF' : '#888888')
              .margin({ right: 8 })
          })
        }
        .margin({ bottom: 20 })

        // 解码状态信息
        Column() {
          Text('解码信息').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#4FC3F7')
          
          Grid() {
            GridItem() {
              this.InfoItem('格式', this.decoderState.format)
            }
            GridItem() {
              this.InfoItem('采样率', `${this.decoderState.sampleRate}Hz`)
            }
            GridItem() {
              this.InfoItem('声道数', `${this.decoderState.channelCount}`)
            }
            GridItem() {
              this.InfoItem('已解码帧', `${this.decoderState.decodedFrames}`)
            }
          }
          .columnsTemplate('1fr 1fr')
          .rowsTemplate('1fr 1fr')
          .width('100%')
          .height(120)
        }
        .width('100%')
        .padding(15)
        .borderRadius(12)
        .backgroundColor('#16213e')
        .margin({ bottom: 20 })

        // 控制按钮
        Row() {
          Button(this.decoderState.isDecoding ? '停止解码' : '开始解码')
            .onClick(() => {
              if (this.decoderState.isDecoding) {
                this.stopDecode();
              } else {
                this.startDecode();
              }
            })
            .width(160)
            .height(50)
            .backgroundColor(this.decoderState.isDecoding ? '#EF5350' : '#6C63FF')
            .borderRadius(12)
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0d0d1a')
  }

  @Builder
  InfoItem(label: string, value: string) {
    Column() {
      Text(value).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E0E0E0')
      Text(label).fontSize(11).fontColor('#888888').margin({ top: 4 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  aboutToDisappear() {
    this.stopDecode();
  }
}

3.3 高级:编解码性能优化器

在实时音频场景(如语音通话、直播)中,编解码性能至关重要。下面的示例展示了如何构建一个自适应的编解码性能优化器。

// 文件名:CodecOptimizer.ets
// 功能:自适应编解码性能优化器

import { media } from '@kit.MediaKit';

// 性能指标
interface CodecPerformance {
  encodeLatency: number;    // 编码延迟(ms)
  decodeLatency: number;    // 解码延迟(ms)
  cpuUsage: number;         // CPU 占用率(%)
  memoryUsage: number;      // 内存占用(MB)
  frameDropRate: number;    // 丢帧率(%)
}

// 优化策略
interface OptimizationStrategy {
  useHardware: boolean;       // 是否使用硬件编解码
  targetBitrate: number;      // 目标码率
  targetSampleRate: number;   // 目标采样率
  targetChannelCount: number; // 目标声道数
  bufferSize: number;         // 缓冲区大小
}

// 性能等级
type PerformanceLevel = 'ultra' | 'high' | 'medium' | 'low';

class AdaptiveCodecOptimizer {
  private currentLevel: PerformanceLevel = 'high';
  private performanceHistory: CodecPerformance[] = [];
  private maxHistoryLength = 30; // 保留最近 30 次性能记录

  // 性能等级对应的编解码配置
  private levelConfigs: Record<PerformanceLevel, OptimizationStrategy> = {
    ultra: {
      useHardware: true,
      targetBitrate: 256000,
      targetSampleRate: 48000,
      targetChannelCount: 2,
      bufferSize: 8192,
    },
    high: {
      useHardware: true,
      targetBitrate: 128000,
      targetSampleRate: 44100,
      targetChannelCount: 2,
      bufferSize: 4096,
    },
    medium: {
      useHardware: true,
      targetBitrate: 64000,
      targetSampleRate: 44100,
      targetChannelCount: 1,  // 降为单声道
      bufferSize: 2048,
    },
    low: {
      useHardware: false,     // 低端设备可能不支持硬编
      targetBitrate: 32000,
      targetSampleRate: 16000,
      targetChannelCount: 1,
      bufferSize: 1024,
    },
  };

  // 记录性能数据
  recordPerformance(perf: CodecPerformance) {
    this.performanceHistory.push(perf);
    if (this.performanceHistory.length > this.maxHistoryLength) {
      this.performanceHistory.shift();
    }
    // 根据性能数据自适应调整等级
    this.adaptLevel();
  }

  // 自适应调整性能等级
  private adaptLevel() {
    if (this.performanceHistory.length < 5) return;

    // 取最近 5 次的平均值
    const recent = this.performanceHistory.slice(-5);
    const avgCpu = recent.reduce((sum, p) => sum + p.cpuUsage, 0) / recent.length;
    const avgLatency = recent.reduce((sum, p) => sum + p.encodeLatency + p.decodeLatency, 0) / recent.length;
    const avgDropRate = recent.reduce((sum, p) => sum + p.frameDropRate, 0) / recent.length;

    // 决策逻辑
    if (avgCpu > 80 || avgLatency > 100 || avgDropRate > 5) {
      // 性能不足,降低等级
      this.downgrade();
    } else if (avgCpu < 40 && avgLatency < 30 && avgDropRate < 1) {
      // 性能充裕,可以提升等级
      this.upgrade();
    }
  }

  // 降级
  private downgrade() {
    const levels: PerformanceLevel[] = ['ultra', 'high', 'medium', 'low'];
    const currentIndex = levels.indexOf(this.currentLevel);
    if (currentIndex < levels.length - 1) {
      this.currentLevel = levels[currentIndex + 1];
      console.warn(`[Optimizer] 性能不足,降级到 ${this.currentLevel}`);
    }
  }

  // 升级
  private upgrade() {
    const levels: PerformanceLevel[] = ['ultra', 'high', 'medium', 'low'];
    const currentIndex = levels.indexOf(this.currentLevel);
    if (currentIndex > 0) {
      this.currentLevel = levels[currentIndex - 1];
      console.info(`[Optimizer] 性能充裕,升级到 ${this.currentLevel}`);
    }
  }

  // 获取当前优化策略
  getCurrentStrategy(): OptimizationStrategy {
    return this.levelConfigs[this.currentLevel];
  }

  // 获取当前性能等级
  getCurrentLevel(): PerformanceLevel {
    return this.currentLevel;
  }

  // 获取性能报告
  getPerformanceReport(): string {
    const strategy = this.getCurrentStrategy();
    return [
      `当前等级: ${this.currentLevel}`,
      `硬件编解码: ${strategy.useHardware ? '是' : '否'}`,
      `目标码率: ${strategy.targetBitrate}bps`,
      `采样率: ${strategy.targetSampleRate}Hz`,
      `声道数: ${strategy.targetChannelCount}`,
      `缓冲区: ${strategy.bufferSize}`,
    ].join('\n');
  }
}

// ==================== UI 组件 ====================

@Entry
@Component
struct CodecOptimizerPage {
  private optimizer: AdaptiveCodecOptimizer = new AdaptiveCodecOptimizer();

  @State currentLevel: string = 'high';
  @State strategyInfo: string = '';
  @State isRunning: boolean = false;
  @State performanceLog: string[] = [];

  aboutToAppear() {
    this.updateDisplay();
  }

  // 更新显示信息
  private updateDisplay() {
    this.currentLevel = this.optimizer.getCurrentLevel();
    this.strategyInfo = this.optimizer.getPerformanceReport();
  }

  // 启动性能监控
  startMonitoring() {
    this.isRunning = true;
    this.simulatePerformance();
  }

  // 模拟性能数据采集
  private simulatePerformance() {
    if (!this.isRunning) return;

    // 模拟不同负载下的性能指标
    const cpuBase = this.currentLevel === 'low' ? 30 : this.currentLevel === 'medium' ? 50 : 70;
    const perf: CodecPerformance = {
      encodeLatency: Math.random() * 20 + 5,
      decodeLatency: Math.random() * 15 + 3,
      cpuUsage: cpuBase + Math.random() * 30,
      memoryUsage: 20 + Math.random() * 30,
      frameDropRate: Math.random() * 3,
    };

    this.optimizer.recordPerformance(perf);

    // 添加日志
    const logEntry = `[${new Date().toLocaleTimeString()}] CPU: ${perf.cpuUsage.toFixed(1)}% | ` +
      `编码延迟: ${perf.encodeLatency.toFixed(1)}ms | 等级: ${this.optimizer.getCurrentLevel()}`;
    this.performanceLog = [logEntry, ...this.performanceLog].slice(0, 20);

    this.updateDisplay();

    // 持续监控
    setTimeout(() => this.simulatePerformance(), 1000);
  }

  // 停止监控
  stopMonitoring() {
    this.isRunning = false;
  }

  build() {
    Scroll() {
      Column() {
        Text('编解码性能优化器')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')
          .margin({ bottom: 20 })

        // 当前性能等级指示器
        Row() {
          ForEach(['ultra', 'high', 'medium', 'low'] as PerformanceLevel[], (level: PerformanceLevel) => {
            Column() {
              Circle({ width: 16, height: 16 })
                .fill(this.currentLevel === level ? '#6C63FF' : '#333333')
              Text(level.toUpperCase())
                .fontSize(10)
                .fontColor(this.currentLevel === level ? '#6C63FF' : '#666666')
                .margin({ top: 4 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .padding(15)
        .borderRadius(12)
        .backgroundColor('#16213e')
        .margin({ bottom: 15 })

        // 当前策略信息
        Column() {
          Text('当前优化策略').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#4FC3F7')
          Text(this.strategyInfo)
            .fontSize(13)
            .fontColor('#AAAAAA')
            .margin({ top: 10 })
            .lineHeight(22)
        }
        .width('100%')
        .padding(15)
        .borderRadius(12)
        .backgroundColor('#16213e')
        .alignItems(HorizontalAlign.Start)
        .margin({ bottom: 15 })

        // 性能日志
        Column() {
          Text('性能日志').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#4FC3F7')
          Column() {
            ForEach(this.performanceLog, (log: string, index: number) => {
              Text(log)
                .fontSize(11)
                .fontColor(index === 0 ? '#E0E0E0' : '#666666')
                .width('100%')
                .padding({ top: 4, bottom: 4 })
            })
          }
          .width('100%')
          .maxHeight(200)
        }
        .width('100%')
        .padding(15)
        .borderRadius(12)
        .backgroundColor('#16213e')
        .alignItems(HorizontalAlign.Start)
        .margin({ bottom: 15 })

        // 控制按钮
        Button(this.isRunning ? '停止监控' : '开始监控')
          .onClick(() => {
            if (this.isRunning) {
              this.stopMonitoring();
            } else {
              this.startMonitoring();
            }
          })
          .width('100%')
          .height(50)
          .backgroundColor(this.isRunning ? '#EF5350' : '#6C63FF')
          .borderRadius(12)
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0d0d1a')
  }
}

四、踩坑与注意事项

4.1 编解码常见陷阱

陷阱 现象 解决方案
缓冲区大小不匹配 解码输出数据截断或噪声 根据采样率和声道数精确计算缓冲区大小
采样格式不统一 编码器输出和解码器输入格式不一致 统一使用 SAMPLE_S16LESAMPLE_F32LE
硬件解码器不可用 在部分低端设备上创建解码器失败 先查询 getAudioDecoderCaps(),做好软解降级
Muxer 封装格式错误 AAC 数据封装为 MP4 但无法播放 确保 ContainerFormatType 与 MIME 类型匹配
内存泄漏 长时间编解码后内存持续增长 及时调用 releaseOutputBuffer()releaseInputBuffer()

4.2 缓冲区大小计算

// PCM 缓冲区大小计算公式
// bufferSize = sampleRate × channelCount × bytesPerSample × durationSeconds

function calculatePcmBufferSize(
  sampleRate: number,
  channelCount: number,
  sampleFormat: media.AudioSampleFormat,
  durationMs: number
): number {
  // 每个采样的字节数
  const bytesPerSample = sampleFormat === media.AudioSampleFormat.SAMPLE_S16LE ? 2 :
                         sampleFormat === media.AudioSampleFormat.SAMPLE_S24LE ? 3 :
                         sampleFormat === media.AudioSampleFormat.SAMPLE_S32LE ? 4 :
                         sampleFormat === media.AudioSampleFormat.SAMPLE_F32LE ? 4 : 2;

  return Math.ceil(sampleRate * channelCount * bytesPerSample * durationMs / 1000);
}

// 示例:44100Hz, 立体声, 16bit, 20ms 一帧
// bufferSize = 44100 × 2 × 2 × 0.02 = 3528 字节

4.3 硬件编解码选择指南

// 硬件编解码选择决策树
function shouldUseHardwareCodec(
  mimeType: string,
  isRealTime: boolean,
  deviceLevel: 'high' | 'medium' | 'low'
): boolean {
  // 1. 低端设备不建议使用硬编(可能不支持)
  if (deviceLevel === 'low') return false;

  // 2. FLAC 无损格式通常没有硬件编解码器
  if (mimeType === 'audio/flac') return false;

  // 3. 实时场景优先使用硬件编解码
  if (isRealTime) return true;

  // 4. AAC 格式硬件编解码支持最好
  if (mimeType === 'audio/aac') return true;

  // 5. MP3 硬件解码支持有限,建议软解
  if (mimeType === 'audio/mpeg') return false;

  return false;
}

五、HarmonyOS 6 适配

5.1 版本差异

特性 HarmonyOS 5 (API 12) HarmonyOS 6 (API 14)
硬件编解码 仅 AAC 硬编硬解 新增:MP3 硬件解码、Opus 编解码支持
CodecBase 基础接口 增强:新增 getCodecProfile() 支持查询编码器配置
性能监控 无系统接口 新增:CodecPerformanceMonitor 系统级性能监控
低延迟编解码 不支持 新增:lowLatency 模式,编解码延迟降低 40%
多实例并发 最多 2 个 新增:支持最多 8 个编解码实例并发

5.2 迁移指南

// HarmonyOS 5 写法——手动管理编解码实例
const encoder = await media.createAudioEncoderByName('avcodec_aac_encoder');
// ... 编码逻辑 ...

// HarmonyOS 6 写法——使用新的性能监控和低延迟模式
// const encoder = await media.createAudioEncoderByName('avcodec_aac_encoder');
// const perfMonitor = media.createCodecPerformanceMonitor();
// perfMonitor.on('performanceUpdate', (perf: media.CodecPerformanceInfo) => {
//   console.info(`编码延迟: ${perf.encodeLatency}ms, CPU: ${perf.cpuUsage}%`);
// });
//
// // 启用低延迟模式
// const config: media.AVCodecConfig = {
//   audioConfig: {
//     sampleRate: 48000,
//     channelCount: 1,
//     bitRate: 64000,
//     sampleFormat: media.AudioSampleFormat.SAMPLE_S16LE,
//   },
//   mimeType: 'audio/aac',
//   lowLatency: true,  // ★ 新增:低延迟模式
// };

5.3 注意事项

  • HarmonyOS 6 的 lowLatency 模式会降低编码效率(码率相同时音质略降),适合语音通话场景
  • CodecPerformanceMonitor 的回调频率为每秒 1 次,不会影响编解码性能
  • 多实例并发时注意总内存占用,每个实例约占用 5-10MB 内存

六、总结

mindmap
  root((音频编解码))
    核心概念
      编码 PCM→压缩格式
      解码 压缩格式→PCM
      CodecBase 抽象层
    主流格式
      AAC 有损低延迟
      MP3 有损兼容性好
      FLAC 无损高质量
    硬件vs软件
      硬件 低功耗低延迟
      软件 兼容性好
      按场景选择
    性能优化
      自适应码率
      缓冲区优化
      降级策略
      性能监控
    踩坑点
      缓冲区大小计算
      采样格式统一
      内存泄漏防护
    HarmonyOS 6
      MP3硬解支持
      Opus编解码
      低延迟模式
      多实例并发

    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

关键知识点回顾

  1. AAC 是鸿蒙上最推荐的编解码格式——硬件支持好、延迟低、压缩率高
  2. 硬件编解码优先,软件编解码兜底——务必做好降级方案
  3. 缓冲区大小必须精确计算——公式:sampleRate × channelCount × bytesPerSample × duration
  4. 长时间编解码必须监控性能——CPU 占用、延迟、丢帧率是三大核心指标
  5. HarmonyOS 6 新增低延迟模式和多实例并发——实时音频场景的福音

编解码是音频开发的"地基",地基打好了,上层的播放、录制、通话才能稳如磐石。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。