如何实现扬声器听筒的切换
============

#### 问题现象

音频播放场景，有没有API可以切换扬声器或听筒播放声音？

#### 背景知识

应用可通过AVCastPicker或setDefaultOutputDevice实现音频输出设备路由切换，可参考[实现音频输出设备路由切换](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/audio-output-device-switcher)。

#### 解决方案

*   **方案一：[AVCastPicker](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ohos-multimedia-avcastpicker#avcastpicker)投播组件**：
    *   支持用户在页面手动切换不同的音频输出设备。
    *   支持有线耳机、蓝牙耳机等外接设备间的切换。
*   **方案二：setDefaultOutputDevice：**
    *   包括AudioRenderer的[setDefaultOutputDevice](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiorenderer#setdefaultoutputdevice12)（C接口[OH\_AudioRenderer\_SetDefaultOutputDevice](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/capi-native-audiorenderer-h#oh_audiorenderer_setdefaultoutputdevice)）和AudioSessionManager的[setDefaultOutputDevice](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiosessionmanager#setdefaultoutputdevice20)（C接口[OH\_AudioSessionManager\_SetDefaultOutputDevice](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/capi-native-audio-session-manager-h#oh_audiosessionmanager_setdefaultoutputdevice)）。
    *   当使用AudioRenderer播放音频时，可直接使用setDefaultOutputDevice切换扬声器和听筒，但优先级低于AudioSessionManager的setDefaultOutputDevice。
    *   三方SDK播放器、AVPlayer等其他播放方式，可使用AudioSessionManager的setDefaultOutputDevice实现，使用前需要调用[activateAudioSession](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiosessionmanager#activateaudiosession12)激活。
    *   AudioSessionManager的setDefaultOutputDevice优先级低于AVCastPicker。如果使用AVCastPicker切换过发声设备，再次调用AudioSessionManager切换设备将不生效。
    *   仅支持VOIP场景，即[StreamUsage](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-e#streamusage)为语音消息、VOIP语音通话或者VOIP视频通话的场景。如果无法配置音频流参数，可调用[setAudioSessionScene](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiosessionmanager#setaudiosessionscene20)设置音频会话场景[AudioSessionScene](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-e#audiosessionscene20)为VoIP语音通话音频会话场景。
    *   插入蓝牙耳机等外接设备时，不支持切换。

使用示例代码如下：

import { audio } from '@kit.AudioKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AVCastPicker, avSession } from '@kit.AVSessionKit';


let audioRenderer: audio.AudioRenderer;
let audioStreamInfo: audio.AudioStreamInfo = {
  samplingRate: audio.AudioSamplingRate.SAMPLE\_RATE\_8000, _// 采样率。_
  channels: audio.AudioChannel.CHANNEL\_1, _// 通道。_
  sampleFormat: audio.AudioSampleFormat.SAMPLE\_FORMAT\_S16LE, _// 采样格式。_
  encodingType: audio.AudioEncodingType.ENCODING\_TYPE\_RAW _// 编码格式。_
};
let audioRendererInfo: audio.AudioRendererInfo = {
  usage: audio.StreamUsage.STREAM\_USAGE\_VOICE\_COMMUNICATION, _// 音频流使用类型_
  rendererFlags: 0 _// 音频渲染器标志。_
};
let audioRendererOptions: audio.AudioRendererOptions = {
  streamInfo: audioStreamInfo,
  rendererInfo: audioRendererInfo
};


let audioManager = audio.getAudioManager(); _// 需要先创建AudioManager实例。_
let audioSessionManager = audioManager.getSessionManager(); _// 再调用AudioManager的方法创建AudioSessionManager实例。_
let strategy: audio.AudioSessionStrategy = {
  concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY\_PAUSE\_OTHERS
};
let sessionAv: avSession.AVSession;


@Entry
@Component
export struct SwitchAudioDeviceDemo {
  context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  audioData: Uint8Array = genTestPCM(); _// 测试PCM数据，按需替换为其他音频数据源_
  writeOffset = 0;
  @State renderIsSpeaker: boolean = false;
  @State sessionIsSpeaker: boolean = false;


  async aboutToAppear(): Promise<void> {
    audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
    await this.init(); _//初始化_

    audioSessionManager.activateAudioSession(strategy).then(() => { _//激活audioSession_
      console.info('Succeeded in activating audio session.');
    }).catch((err: BusinessError) => {
      console.error(\`Failed to activate audio session. Code: ${err.code}, message: ${err.message}\`);
    });


    sessionAv = await avSession.createAVSession(this.context, 'voiptest', 'voice\_call');
  }


  async aboutToDisappear(): Promise<void> {
    await audioRenderer.release();


    await sessionAv.destroy();


    await audioSessionManager.deactivateAudioSession().then(() => {
      console.info('Succeeded in deactivating audio session.');
    }).catch((err: BusinessError) => {
      console.error(\`Failed to deactivate audio session. Code: ${err.code}, message: ${err.message}\`);
    });
  }


  build() {
    Column({ space: 10 }) {
      Button('播放音频数据')
        .width('100%')
        .onClick(async () => {
          await audioRenderer.start();
        });
      Button('停止播放')
        .width('100%')
        .onClick(async () => {
          console.info('renderer status' + audioRenderer.state);
          this.stopAndFlush();
        });


      Row() {
        Text('AudioRenderer切换播放设备：');
        Toggle({ type: ToggleType.Switch, isOn: this.renderIsSpeaker })
          .onChange((isOn: boolean) => {
            this.renderIsSpeaker = isOn;
            audioRenderer.setDefaultOutputDevice(isOn ? audio.DeviceType.SPEAKER :
              audio.DeviceType.EARPIECE)
              .then(() => {
                console.info('AudioRenderer Succeeded in setting default output device.');
              }).catch((err: BusinessError) => {
              console.error(\`AudioRenderer setDefaultOutputDevice Fail: ${err}\`);
            });
          });
        Text(\`${this.renderIsSpeaker ? '扬声器' : '听筒'}\`);
      }
      .width('100%')
      .justifyContent(FlexAlign.Start);


      Row() {
        Text('AudioSession   切换播放设备：');
        Toggle({ type: ToggleType.Switch, isOn: this.sessionIsSpeaker })
          .onChange((isOn: boolean) => {
            this.sessionIsSpeaker = isOn;
            audioSessionManager.setDefaultOutputDevice(isOn ? audio.DeviceType.SPEAKER :
              audio.DeviceType.EARPIECE)
              .then(() => {
                console.info('AudioSession Succeeded in setting default output device.');
              }).catch((err: BusinessError) => {
              console.error(\`AudioSession setDefaultOutputDevice Fail: ${err}\`);
            });
          });
        Text(\`${this.sessionIsSpeaker ? '扬声器' : '听筒'}\`);
      }
      .width('100%')
      .justifyContent(FlexAlign.Start);


      AVCastPicker({}).width('50').height('50');
    }
    .padding(20)
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%');
  }


  async init() {
    audioRenderer.on('writeData', (buffer: ArrayBuffer) => {
      if (!this.audioData) {
        return audio.AudioDataCallbackResult.INVALID;
      }
      let bufferView = new Uint8Array(buffer);
      let writeLen = Math.min(buffer.byteLength, this.audioData.byteLength - this.writeOffset);
      if (writeLen <= 0) {
        this.writeOffset = 0;
        console.info('Play Done');
        return audio.AudioDataCallbackResult.INVALID;
      }
      bufferView.set(this.audioData.slice(this.writeOffset, this.writeOffset + writeLen));
      this.writeOffset += writeLen;
      return audio.AudioDataCallbackResult.VALID;
    });
  }


  async stopAndFlush() {
    console.info('renderer status' + audioRenderer.state);
    audioRenderer.stop().then(() => {
      console.error('Renderer stop ok.');
    }).catch((err: BusinessError) => {
      console.error('Renderer stop failed. ', err);
    });
    audioRenderer.flush().then(() => {
      console.error('Renderer flush ok.');
    }).catch((err: BusinessError) => {
      console.error('renderer flush err. ' + err);
    });
    this.writeOffset = 0;
  }
}


function genTestPCM(): Uint8Array {
  const sampleRate = 8000;
  const noteDuration = 0.5;
  const amplitude = 0.35;


  const freqMap: Record<number, number> = {
    1: 523.25, _// C5_
    2: 587.33, _// D5_
    3: 659.25, _// E5_
    4: 698.46, _// F5_
    5: 783.99, _// G5_
    6: 880.00, _// A5_
  };


  const melody = \[
    1, 1, 5, 5, 6, 6, 5, 0,
    4, 4, 3, 3, 2, 2, 1, 0,
    5, 5, 4, 4, 3, 3, 2, 0
  \];


  const samplesPerNote = Math.floor(sampleRate \* noteDuration); _// 4000_
  const totalSamples = samplesPerNote \* melody.length; _// 96,000_
  const buffer = new ArrayBuffer(totalSamples \* 2); _// 192,000 bytes_
  const view = new DataView(buffer);


  let idx = 0;
  for (const note of melody) {
    const freq = note ? freqMap\[note\] : 0;
    for (let i = 0; i < samplesPerNote; i++) {
      const t = i / sampleRate;
      const wave = freq ? amplitude \* Math.sin(2 \* Math.PI \* freq \* t) : 0;
      const sample = Math.round(wave \* 32767);
      const clamped = Math.max(-32768, Math.min(32767, sample));
      view.setInt16(idx \* 2, clamped, true);
      idx++;
    }
  }
  return new Uint8Array(buffer);
}