走进HarmonyOS开发中的声音之谜

举报
Jack20 发表于 2026/06/20 19:24:48 2026/06/20
【摘要】 HarmonyOS APP开发中的声音设置:@ohos.multimedia.audio 全解析——从音量控制到音频焦点,让声音井井有条📌 核心要点:audio 模块提供音量控制、音频路由、静音模式和音频焦点管理,是构建音频应用和系统级声音设置的核心基础设施 一、背景与动机想象一下这个场景——你正在用手机听音乐,突然来了个电话,音乐自动淡出,铃声响起;通话结束后,音乐又自动淡入继续播放。...

HarmonyOS APP开发中的声音设置:@ohos.multimedia.audio 全解析——从音量控制到音频焦点,让声音井井有条

📌 核心要点:audio 模块提供音量控制、音频路由、静音模式和音频焦点管理,是构建音频应用和系统级声音设置的核心基础设施


一、背景与动机

想象一下这个场景——你正在用手机听音乐,突然来了个电话,音乐自动淡出,铃声响起;通话结束后,音乐又自动淡入继续播放。这种丝滑的体验背后,是音频焦点管理在默默工作。

再比如,你正在看视频,想调大音量,结果把铃声音量调大了——因为系统默认调节的是铃声音量,而不是媒体音量。这种"调错音量"的尴尬,是因为音频流类型音量控制没有正确关联。

HarmonyOS 的 @ohos.multimedia.audio 模块,就是为了解决这些问题而存在的。它就像一个"交通警察",管理着各种声音的通行权——谁该响、谁该静、谁该让路,都安排得明明白白。

典型使用场景:

  • 音乐播放器:音频焦点申请与释放、音量控制
  • 视频通话:通话音量独立控制、静音切换
  • 游戏应用:游戏音效与背景音乐混合
  • 系统设置:音量面板、静音模式、音频路由切换

二、核心原理

2.1 音频系统架构

图片.png

2.2 音频流类型(AudioVolumeType)

HarmonyOS 将声音分为不同的流类型,每种类型有独立的音量控制:

流类型 枚举值 说明 典型场景
RINGTONE 2 铃声 来电铃声、通知提示音
MEDIA 3 媒体 音乐、视频、游戏音效
VOICE_CALL 0 语音通话 电话通话
SYSTEM 6 系统 系统按键音、开关机音
ALARM 4 闹钟 闹钟铃声
ACCESSIBILITY 5 辅助功能 屏幕朗读
VOICE_ASSISTANT 9 语音助手 小艺语音

2.3 音频焦点机制

sequenceDiagram
    participant Music as 音乐播放器
    participant System as 音频焦点管理器
    participant Phone as 电话应用

    Music->>System: requestAudioFocus(MEDIA)
    System-->>Music: 焦点授予 ✓
    Note over Music: 播放音乐中...

    Phone->>System: requestAudioFocus(VOICE_CALL)
    System->>Music: onAudioFocusChange(LOST)
    Note over Music: 音乐暂停
    System-->>Phone: 焦点授予 ✓
    Note over Phone: 通话进行中...

    Phone->>System: abandonAudioFocus()
    System->>Music: onAudioFocusChange(GAINED)
    Note over Music: 音乐恢复播放

音频焦点就像会议室的"发言权"——同一时间只有一个人能发言,新来的人要发言,之前的人就得让座。但"让座"的方式有多种:完全暂停、降低音量(duck)、或者只是被通知一下。


三、代码实战

3.1 音量控制:多流类型音量管理

不同流类型的音量是独立的。调节媒体音量不应该影响铃声音量,反之亦然。

import { audio } from '@kit.MultimediaKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 音量管理器
 * 封装各音频流类型的音量读写操作
 */
class VolumeManager {
  private audioManager: audio.AudioManager | null = null;

  /**
   * 初始化音频管理器
   */
  async init(): Promise<void> {
    this.audioManager = audio.getAudioManager();
    console.info('[Volume] 音频管理器初始化成功');
  }

  /**
   * 获取指定流类型的音量
   * @param volumeType 音频流类型
   */
  async getVolume(volumeType: audio.AudioVolumeType): Promise<number> {
    if (!this.audioManager) return 0;
    try {
      const volume = await this.audioManager.getVolume(volumeType);
      console.info(`[Volume] ${this.getTypeName(volumeType)} 音量: ${volume}`);
      return volume;
    } catch (err) {
      console.error(`[Volume] 获取音量失败: ${JSON.stringify(err)}`);
      return 0;
    }
  }

  /**
   * 设置指定流类型的音量
   * @param volumeType 音频流类型
   * @param volume 音量值
   */
  async setVolume(volumeType: audio.AudioVolumeType, volume: number): Promise<void> {
    if (!this.audioManager) return;
    try {
      // 先获取最大音量,做范围校验
      const maxVolume = await this.audioManager.getMaxVolume(volumeType);
      const minVolume = await this.audioManager.getMinVolume(volumeType);
      const clampedVolume = Math.max(minVolume, Math.min(maxVolume, volume));

      await this.audioManager.setVolume(volumeType, clampedVolume);
      console.info(`[Volume] ${this.getTypeName(volumeType)} 音量设置: ${clampedVolume}`);
    } catch (err) {
      console.error(`[Volume] 设置音量失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 获取最大音量
   */
  async getMaxVolume(volumeType: audio.AudioVolumeType): Promise<number> {
    if (!this.audioManager) return 15;
    try {
      return await this.audioManager.getMaxVolume(volumeType);
    } catch (err) {
      return 15;
    }
  }

  /**
   * 获取最小音量
   */
  async getMinVolume(volumeType: audio.AudioVolumeType): Promise<number> {
    if (!this.audioManager) return 0;
    try {
      return await this.audioManager.getMinVolume(volumeType);
    } catch (err) {
      return 0;
    }
  }

  /**
   * 逐步调节音量(模拟物理按键效果)
   * @param volumeType 流类型
   * @param direction 方向: 1=增大, -1=减小
   */
  async stepVolume(volumeType: audio.AudioVolumeType, direction: number): Promise<void> {
    const current = await this.getVolume(volumeType);
    const newVolume = current + direction;
    await this.setVolume(volumeType, newVolume);
  }

  /**
   * 判断指定流类型是否静音
   */
  async isMute(volumeType: audio.AudioVolumeType): Promise<boolean> {
    if (!this.audioManager) return false;
    try {
      return await this.audioManager.isMute(volumeType);
    } catch (err) {
      return false;
    }
  }

  /**
   * 设置指定流类型静音
   */
  async setMute(volumeType: audio.AudioVolumeType, mute: boolean): Promise<void> {
    if (!this.audioManager) return;
    try {
      await this.audioManager.mute(volumeType, mute);
      console.info(`[Volume] ${this.getTypeName(volumeType)} ${mute ? '静音' : '取消静音'}`);
    } catch (err) {
      console.error(`[Volume] 设置静音失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 监听音量变化
   */
  onVolumeChange(callback: (volumeType: audio.AudioVolumeType, volume: number) => void): void {
    if (!this.audioManager) return;
    this.audioManager.on('volumeChange', (volumeEvent: audio.VolumeEvent) => {
      callback(volumeEvent.volumeType, volumeEvent.volume);
    });
  }

  /**
   * 获取流类型名称(用于日志)
   */
  private getTypeName(type: audio.AudioVolumeType): string {
    const nameMap: Record<number, string> = {
      0: '语音通话',
      2: '铃声',
      3: '媒体',
      4: '闹钟',
      5: '辅助功能',
      6: '系统',
      9: '语音助手',
    };
    return nameMap[type] || `未知(${type})`;
  }
}

export { VolumeManager };

3.2 音频路由与静音模式:蓝牙/扬声器切换

音频路由决定了声音从哪里出来——扬声器、耳机还是听筒。静音模式则控制是否允许声音播放。

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

/**
 * 音频路由管理器
 * 管理音频输出设备切换和静音模式
 */
class AudioRouteManager {
  private audioManager: audio.AudioManager | null = null;
  private audioRoutingManager: audio.AudioRoutingManager | null = null;

  /**
   * 初始化
   */
  async init(): Promise<void> {
    this.audioManager = audio.getAudioManager();
    this.audioRoutingManager = this.audioManager.getRoutingManager();
    console.info('[Route] 音频路由管理器初始化成功');
  }

  /**
   * 获取当前输出设备列表
   */
  async getOutputDevices(): Promise<audio.AudioDeviceDescriptors> {
    if (!this.audioRoutingManager) return [];
    try {
      const devices = await this.audioRoutingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG);
      console.info(`[Route] 输出设备数量: ${devices.length}`);
      devices.forEach((device, index) => {
        console.info(`[Route] 设备${index}: ${this.getDeviceName(device.deviceType)}`);
      });
      return devices;
    } catch (err) {
      console.error(`[Route] 获取设备失败: ${JSON.stringify(err)}`);
      return [];
    }
  }

  /**
   * 判断是否连接了蓝牙音频设备
   */
  async hasBluetoothDevice(): Promise<boolean> {
    const devices = await this.getOutputDevices();
    return devices.some(d =>
      d.deviceType === audio.DeviceType.BLUETOOTH_A2DP ||
      d.deviceType === audio.DeviceType.BLUETOOTH_SCO
    );
  }

  /**
   * 判断是否连接了有线耳机
   */
  async hasWiredHeadset(): Promise<boolean> {
    const devices = await this.getOutputDevices();
    return devices.some(d => d.deviceType === audio.DeviceType.WIRED_HEADSET);
  }

  /**
   * 获取当前活跃的输出设备类型
   */
  async getActiveOutputType(): Promise<string> {
    const devices = await this.getOutputDevices();
    if (devices.length === 0) return '无设备';

    // 优先级:蓝牙 > 有线 > 扬声器
    if (devices.some(d => d.deviceType === audio.DeviceType.BLUETOOTH_A2DP)) {
      return '蓝牙耳机';
    }
    if (devices.some(d => d.deviceType === audio.DeviceType.WIRED_HEADSET)) {
      return '有线耳机';
    }
    if (devices.some(d => d.deviceType === audio.DeviceType.EARPIECE)) {
      return '听筒';
    }
    return '扬声器';
  }

  /**
   * 监听音频设备变化
   * 蓝牙连接/断开、耳机插拔等
   */
  onDeviceChange(callback: (deviceChanged: audio.AudioDeviceDescriptors) => void): void {
    if (!this.audioRoutingManager) return;
    this.audioRoutingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG,
      (deviceChanged: audio.AudioDeviceDescriptors) => {
        console.info('[Route] 音频设备变更');
        callback(deviceChanged);
      }
    );
  }

  /**
   * 获取设备名称
   */
  private getDeviceName(type: audio.DeviceType): string {
    const nameMap: Record<number, string> = {
      1: '无效',
      2: '扬声器',
      3: '有线耳机',
      4: '蓝牙A2DP',
      7: '蓝牙SCO',
      8: '听筒',
      14: 'USB',
      17: '蓝牙HFP',
    };
    return nameMap[type] || `未知(${type})`;
  }
}

/**
 * 静音模式管理器
 */
class MuteModeManager {
  private audioManager: audio.AudioManager | null = null;

  async init(): Promise<void> {
    this.audioManager = audio.getAudioManager();
  }

  /**
   * 获取静音状态
   */
  async getMuteStatus(): Promise<Record<string, boolean>> {
    if (!this.audioManager) return {};
    try {
      return {
        media: await this.audioManager.isMute(audio.AudioVolumeType.MEDIA),
        ringtone: await this.audioManager.isMute(audio.AudioVolumeType.RINGTONE),
        system: await this.audioManager.isMute(audio.AudioVolumeType.SYSTEM),
        alarm: await this.audioManager.isMute(audio.AudioVolumeType.ALARM),
      };
    } catch (err) {
      console.error(`[Mute] 获取静音状态失败: ${JSON.stringify(err)}`);
      return {};
    }
  }

  /**
   * 全部静音(勿扰模式)
   */
  async muteAll(): Promise<void> {
    if (!this.audioManager) return;
    try {
      await this.audioManager.mute(audio.AudioVolumeType.MEDIA, true);
      await this.audioManager.mute(audio.AudioVolumeType.RINGTONE, true);
      await this.audioManager.mute(audio.AudioVolumeType.SYSTEM, true);
      // 注意:闹钟通常不静音,避免用户错过重要提醒
      console.info('[Mute] 已进入勿扰模式(闹钟除外)');
    } catch (err) {
      console.error(`[Mute] 全部静音失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 取消全部静音
   */
  async unmuteAll(): Promise<void> {
    if (!this.audioManager) return;
    try {
      await this.audioManager.mute(audio.AudioVolumeType.MEDIA, false);
      await this.audioManager.mute(audio.AudioVolumeType.RINGTONE, false);
      await this.audioManager.mute(audio.AudioVolumeType.SYSTEM, false);
      console.info('[Mute] 已退出勿扰模式');
    } catch (err) {
      console.error(`[Mute] 取消静音失败: ${JSON.stringify(err)}`);
    }
  }
}

/**
 * 音量与路由控制页面
 */
@Entry
@Component
struct AudioSettingPage {
  @State mediaVolume: number = 0;
  @State ringVolume: number = 0;
  @State maxVolume: number = 15;
  @State isMediaMute: boolean = false;
  @State activeOutput: string = '扬声器';

  private volumeManager: VolumeManager = new VolumeManager();
  private routeManager: AudioRouteManager = new AudioRouteManager();

  async aboutToAppear(): Promise<void> {
    await this.volumeManager.init();
    await this.routeManager.init();

    // 读取当前音量
    this.mediaVolume = await this.volumeManager.getVolume(audio.AudioVolumeType.MEDIA);
    this.ringVolume = await this.volumeManager.getVolume(audio.AudioVolumeType.RINGTONE);
    this.maxVolume = await this.volumeManager.getMaxVolume(audio.AudioVolumeType.MEDIA);
    this.isMediaMute = await this.volumeManager.isMute(audio.AudioVolumeType.MEDIA);
    this.activeOutput = await this.routeManager.getActiveOutputType();

    // 监听音量变化
    this.volumeManager.onVolumeChange((type, volume) => {
      if (type === audio.AudioVolumeType.MEDIA) {
        this.mediaVolume = volume;
      } else if (type === audio.AudioVolumeType.RINGTONE) {
        this.ringVolume = volume;
      }
    });

    // 监听设备变化
    this.routeManager.onDeviceChange(async () => {
      this.activeOutput = await this.routeManager.getActiveOutputType();
    });
  }

  build() {
    Scroll() {
      Column() {
        Text('🔊 声音设置')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 24 })

        // 当前输出设备
        Row() {
          Text('当前输出')
            .fontSize(16)
            .layoutWeight(1)
          Text(this.activeOutput)
            .fontSize(16)
            .fontColor('#4CAF50')
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#E8F5E9')
        .borderRadius(12)
        .margin({ bottom: 20 })

        // 媒体音量
        Column() {
          Row() {
            Text('🎵 媒体音量')
              .fontSize(16)
              .layoutWeight(1)
            Text(`${this.mediaVolume}/${this.maxVolume}`)
              .fontSize(14)
              .fontColor('#666')
          }
          .width('100%')

          Slider({
            value: this.mediaVolume,
            min: 0,
            max: this.maxVolume,
            step: 1,
            style: SliderStyle.OutSet
          })
            .width('100%')
            .onChange(async (value: number) => {
              await this.volumeManager.setVolume(audio.AudioVolumeType.MEDIA, value);
            })
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#f5f5f5')
        .borderRadius(12)
        .margin({ bottom: 12 })

        // 铃声音量
        Column() {
          Row() {
            Text('🔔 铃声音量')
              .fontSize(16)
              .layoutWeight(1)
            Text(`${this.ringVolume}/${this.maxVolume}`)
              .fontSize(14)
              .fontColor('#666')
          }
          .width('100%')

          Slider({
            value: this.ringVolume,
            min: 0,
            max: this.maxVolume,
            step: 1,
            style: SliderStyle.OutSet
          })
            .width('100%')
            .onChange(async (value: number) => {
              await this.volumeManager.setVolume(audio.AudioVolumeType.RINGTONE, value);
            })
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#f5f5f5')
        .borderRadius(12)
        .margin({ bottom: 12 })

        // 静音开关
        Row() {
          Text('🔇 媒体静音')
            .fontSize(16)
            .layoutWeight(1)
          Toggle({ type: ToggleType.Switch, isOn: this.isMediaMute })
            .onChange(async (isOn: boolean) => {
              this.isMediaMute = isOn;
              await this.volumeManager.setMute(audio.AudioVolumeType.MEDIA, isOn);
            })
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#f5f5f5')
        .borderRadius(12)
      }
      .padding(20)
    }
    .width('100%')
    .height('100%')
  }
}

export { AudioRouteManager, MuteModeManager };

3.3 音频焦点管理:多应用音频协调

音频焦点是音频系统中最容易被忽视、却又最关键的能力。没有焦点管理,多个应用同时播放声音就是一场灾难。

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

/**
 * 音频焦点类型说明:
 * AUDIOFOCUS_DEFAULT     = 0  默认焦点
 * AUDIOFOCUS_GAIN        = 1  获得焦点(可正常播放)
 * AUDIOFOCUS_LOSS        = 2  永久失去焦点(需停止播放)
 * AUDIOFOCUS_LOSS_TRANSIENT = 3 暂时失去焦点(如通知提示音,稍后可恢复)
 * AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = 4 暂时失去焦点但可降低音量播放
 */

/**
 * 音频焦点管理器
 * 封装焦点申请、释放和变更监听
 */
class AudioFocusManager {
  private audioManager: audio.AudioManager | null = null;
  private audioSessionManager: audio.AudioSessionManager | null = null;
  private currentFocusType: audio.AudioFocusType = audio.AudioFocusType.AUDIOFOCUS_GAIN;
  private isFocused: boolean = false;

  /**
   * 初始化
   */
  async init(): Promise<void> {
    this.audioManager = audio.getAudioManager();
    this.audioSessionManager = this.audioManager.getSessionManager();
    console.info('[Focus] 音频焦点管理器初始化成功');
  }

  /**
   * 申请音频焦点
   * @param focusType 焦点类型
   * @param streamUsage 音频流用途
   */
  async requestFocus(
    focusType: audio.AudioFocusType,
    streamUsage: audio.StreamUsage = audio.StreamUsage.STREAM_USAGE_MUSIC
  ): Promise<boolean> {
    if (!this.audioSessionManager) return false;

    try {
      // 构建焦点请求参数
      const focusRequest: audio.AudioFocusRequest = {
        focusType: focusType,
        streamUsage: streamUsage,
      };

      // 申请焦点
      await this.audioSessionManager.requestAudioFocus(focusRequest);
      this.isFocused = true;
      this.currentFocusType = focusType;
      console.info(`[Focus] 焦点申请成功: type=${focusType}`);
      return true;
    } catch (err) {
      console.error(`[Focus] 焦点申请失败: ${JSON.stringify(err)}`);
      this.isFocused = false;
      return false;
    }
  }

  /**
   * 释放音频焦点
   */
  async abandonFocus(): Promise<void> {
    if (!this.audioSessionManager) return;

    try {
      await this.audioSessionManager.abandonAudioFocus();
      this.isFocused = false;
      console.info('[Focus] 焦点已释放');
    } catch (err) {
      console.error(`[Focus] 焦点释放失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 监听音频焦点变更
   * 当其他应用申请焦点时,当前应用会收到通知
   */
  onFocusChange(callback: (focusType: audio.AudioFocusType) => void): void {
    if (!this.audioSessionManager) return;

    this.audioSessionManager.on('audioFocusChange', (focusChangeInfo: audio.AudioFocusChangeInfo) => {
      const focusType = focusChangeInfo.focusType;
      console.info(`[Focus] 焦点变更: ${this.getFocusName(focusType)}`);

      switch (focusType) {
        case audio.AudioFocusType.AUDIOFOCUS_GAIN:
          // 重新获得焦点,恢复播放
          this.isFocused = true;
          break;
        case audio.AudioFocusType.AUDIOFOCUS_LOSS:
          // 永久失去焦点,停止播放
          this.isFocused = false;
          break;
        case audio.AudioFocusType.AUDIOFOCUS_LOSS_TRANSIENT:
          // 暂时失去焦点,暂停播放
          this.isFocused = false;
          break;
        case audio.AudioFocusType.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
          // 可降低音量继续播放(duck 模式)
          this.isFocused = true;
          break;
      }

      callback(focusType);
    });
  }

  /**
   * 是否持有焦点
   */
  hasFocus(): boolean {
    return this.isFocused;
  }

  /**
   * 获取焦点名称
   */
  private getFocusName(type: audio.AudioFocusType): string {
    const nameMap: Record<number, string> = {
      0: '默认',
      1: '获得焦点',
      2: '永久失去',
      3: '暂时失去',
      4: '可降低音量',
    };
    return nameMap[type] || `未知(${type})`;
  }
}

/**
 * 音乐播放器示例:集成音频焦点
 */
@Entry
@Component
struct MusicPlayerWithFocus {
  @State isPlaying: boolean = false;
  @State focusStatus: string = '未申请';
  @State volumeDucked: boolean = false;

  private focusManager: AudioFocusManager = new AudioFocusManager();

  async aboutToAppear(): Promise<void> {
    await this.focusManager.init();

    // 监听焦点变更
    this.focusManager.onFocusChange((focusType) => {
      switch (focusType) {
        case audio.AudioFocusType.AUDIOFOCUS_GAIN:
          this.focusStatus = '已获得焦点';
          this.volumeDucked = false;
          // 恢复正常播放
          break;
        case audio.AudioFocusType.AUDIOFOCUS_LOSS:
          this.focusStatus = '焦点被抢占';
          this.isPlaying = false;
          // 停止播放
          break;
        case audio.AudioFocusType.AUDIOFOCUS_LOSS_TRANSIENT:
          this.focusStatus = '暂时失去焦点';
          this.isPlaying = false;
          // 暂停播放,等待恢复
          break;
        case audio.AudioFocusType.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
          this.focusStatus = '降低音量播放';
          this.volumeDucked = true;
          // 降低音量继续播放
          break;
      }
    });
  }

  /**
   * 开始播放
   */
  async startPlay(): Promise<void> {
    // 先申请焦点
    const granted = await this.focusManager.requestFocus(
      audio.AudioFocusType.AUDIOFOCUS_GAIN,
      audio.StreamUsage.STREAM_USAGE_MUSIC
    );

    if (granted) {
      this.isPlaying = true;
      this.focusStatus = '已获得焦点';
      console.info('[Player] 开始播放');
    } else {
      this.focusStatus = '焦点申请被拒';
      console.warn('[Player] 无法获取焦点,播放失败');
    }
  }

  /**
   * 停止播放
   */
  async stopPlay(): Promise<void> {
    this.isPlaying = false;
    // 释放焦点
    await this.focusManager.abandonFocus();
    this.focusStatus = '已释放焦点';
  }

  build() {
    Column() {
      Text('🎵 音乐播放器')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 24 })

      // 焦点状态
      Row() {
        Text('焦点状态:')
          .fontSize(14)
          .fontColor('#666')
        Text(this.focusStatus)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.focusManager.hasFocus() ? '#4CAF50' : '#F44336')
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#f5f5f5')
      .borderRadius(8)
      .margin({ bottom: 16 })

      // Duck 提示
      if (this.volumeDucked) {
        Row() {
          Text('⚠️ 音频被降低(有更高优先级的声音正在播放)')
            .fontSize(12)
            .fontColor('#FF9800')
        }
        .width('100%')
        .padding(8)
        .backgroundColor('#FFF3E0')
        .borderRadius(8)
        .margin({ bottom: 16 })
      }

      // 播放控制
      Row() {
        Button(this.isPlaying ? '⏸ 暂停' : '▶ 播放')
          .fontSize(16)
          .backgroundColor(this.isPlaying ? '#FF9800' : '#4CAF50')
          .onClick(async () => {
            if (this.isPlaying) {
              await this.stopPlay();
            } else {
              await this.startPlay();
            }
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }

  async aboutToDisappear(): Promise<void> {
    if (this.isPlaying) {
      await this.stopPlay();
    }
  }
}

export { AudioFocusManager };

四、踩坑与注意事项

4.1 音量设置需要系统权限

:应用内调用 setVolume() 报权限错误。

setVolume() 需要 ohos.permission.ACCESS_NOTIFICATION_POLICY 系统权限。普通应用只能读取音量,不能修改。如果需要调节音量,建议引导用户通过系统音量面板操作。

4.2 静音不等于音量为 0

:把音量设为 0 和静音是两个不同的状态。取消静音后恢复的是静音前的音量,而不是 0。

:使用 mute() 方法管理静音状态,不要通过设置音量为 0 来模拟静音。

4.3 音频焦点必须主动申请

:直接播放音频而不申请焦点,导致与其他应用声音冲突。

:播放前务必申请焦点,播放结束或暂停时释放焦点。

// ✅ 正确流程
async play() {
  const granted = await this.focusManager.requestFocus(
    audio.AudioFocusType.AUDIOFOCUS_GAIN
  );
  if (granted) {
    // 开始播放
  }
}

async pause() {
  // 暂停时可以保持焦点,也可以释放
  // 取决于业务需求
}

async stop() {
  // 停止时务必释放焦点
  await this.focusManager.abandonFocus();
}

4.4 蓝牙设备切换延迟

:蓝牙耳机连接后,音频路由切换有 1-2 秒延迟,这段时间声音可能从扬声器漏出。

:监听 deviceChange 事件,在检测到蓝牙设备后延迟 500ms 再开始播放。

4.5 Duck 模式的音量控制

:收到 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 后,需要自行降低音量,系统不会自动处理。

:在 duck 回调中手动降低播放音量(如降低到 20%),焦点恢复后再调回。


五、HarmonyOS 6 适配

5.1 API 变化一览

变化项 HarmonyOS 5 HarmonyOS 6
AudioSessionManager 基础焦点管理 新增 Session 分组,支持多流协同
空间音频 不支持 新增 SpatialAudioManager 空间音频
音频Offload 不支持 新增音频 Offload 低功耗播放
多声道 立体声 新增 5.1/7.1 多声道支持
音频特效 基础均衡器 新增 AudioEffectManager 音频特效管理

5.2 迁移指南

// HarmonyOS 5:基础焦点申请
const focusRequest: audio.AudioFocusRequest = {
  focusType: audio.AudioFocusType.AUDIOFOCUS_GAIN,
  streamUsage: audio.StreamUsage.STREAM_USAGE_MUSIC,
};
await audioSessionManager.requestAudioFocus(focusRequest);

// HarmonyOS 6:新增 Session 分组
const session = await audioSessionManager.createSession({
  sessionType: audio.AudioSessionType.AUDIO_SESSION_TYPE_MEDIA,
  concurrencyType: audio.AudioConcurrencyType.CONCURRENCY_TYPE_EXCLUSIVE,
});
await session.requestFocus(focusRequest);

5.3 注意事项

  1. Session 分组:HarmonyOS 6 的 Session 机制允许将多个音频流归入同一组,统一管理焦点。比如将背景音乐和音效归入同一 Session,它们会一起获得或失去焦点。
  2. 空间音频:需要设备硬件支持,使用前检查 isSpatialAudioSupported()
  3. 音频 Offload:将音频解码交给 DSP 处理,降低 CPU 功耗。适合长时间播放场景(如音乐、播客)。

六、总结

mindmap
  root((声音设置))
    音量控制
      多流类型独立控制
      最大/最小音量
      音量变化监听
      静音与勿扰
    音频路由
      扬声器/听筒
      有线耳机
      蓝牙A2DP/SCO
      设备变更监听
    静音模式
      按流类型静音
      勿扰模式
      闹钟例外
    音频焦点
      焦点申请/释放
      永久失去/暂时失去
      Duck 降低音量
      多应用协调
    注意事项
      setVolume 需系统权限
      静音≠音量为0
      焦点必须主动申请
      蓝牙切换延迟
      Duck 需自行降音量
    HarmonyOS 6
      Session 分组
      空间音频
      音频 Offload
      多声道支持
      音频特效管理
知识点 要点
音量控制 按流类型独立控制,媒体/铃声/通话各有各的音量
音频路由 声音从哪里出来由路由决定,监听设备变化做适配
静音模式 使用 mute() 而非设置音量为 0,勿扰模式保留闹钟
音频焦点 播放前申请、停止时释放,处理四种焦点变更类型
Duck 模式 收到 CAN_DUCK 时自行降低音量,恢复时调回
HarmonyOS 6 Session 分组、空间音频、Offload 低功耗播放

声音管理就像指挥一个交响乐团——每种乐器(流类型)有自己的音量,每个声部(路由)有自己的位置,而音频焦点就是指挥棒,决定谁该演奏、谁该暂停。掌握了这些,你的应用就能奏出和谐的乐章。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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