如何使用SDK播放音频裸数据或本地文件

本文档详细说明如何基于AICallKit SDK,通过底层AliRtcEngine实现音频裸数据(如PCM)及本地音频文件(如WAV/MP3)的低延迟、无回声播放。

功能介绍

在通话过程中,若需播放音效或背景音,AICallKit SDK 提供了底层的 AliRtcEngine 引擎对象。您可获取该对象并调用其 API 播放音频,借助 RTC 引擎内置的音频处理和回声消除(AEC)能力,有效防止播放的声音被麦克风重新采集,从而避免回声问题。

前提条件

功能实现

AICallKit SDK并未直接提供播放音频接口,它依赖于AliVCSDK_ARTC所提供的外推音频裸数据API,或播放伴奏API。您可以基于AICallKit获取到AliRtcEngine引擎对象,并在需要音频播放时,调用AliRtcEngine的接口实现音频播放,整体交互流程如下:

image

缓存AliRtcEngine引擎对象,必要时提前启动播放器

可以在AICallKit SDKonAliRtcEngineCreated回调中获取ARTC SDK引擎对象AliRtcEngine,如果需要在接通过程中播放音频,则需要提前启动播放器。示例代码如下:

Android

@Override
public void onAliRtcEngineCreated(AliRtcEngine engine) {
    if(engine != null) {
        //获取AliRtcEngine对象engine
        mRtcEngine = engine;

        // 如果需要在接通过程中播放音频,则需要提前启动播放器
        mRtcEngine.startAudioPlayer();
    }
}

iOS

public func onAICallRTCEngineCreated() {
    guard let engine = self.engine.getRTCInstance() as? AliRtcEngine else {
        return
    }
    // 保存AliRtcEngine对象engine
    self.rtcEngine = engine

    // 如果需要在接通过程中播放音频,则需要提前启动播放器
    self.rtcEngine?.startAudioPlayer()
}

场景一: 播放PCM数据

该步骤适用于App通过长链接从服务端接收到音频裸数据后的播放场景。

  1. AliRtcEngine对象添加外部输入流。

    Android

    AliRtcEngine.AliRtcExternalAudioStreamConfig config = new AliRtcEngine.AliRtcExternalAudioStreamConfig();
    config.channels = mPcmChannels;    //PCM声道数
    config.sampleRate = mPcmSampleRate;//PCM采样率
    config.playoutVolume = 100;
    config.publishVolume = 0;  //只播放,不推流
    mStreamId = mAliRtcEngine.addExternalAudioStream(config);
    if mStreamId <= 0 {
        debugPrint("addExternalAudioStream failed: \(mStreamId)")
        return
    }

    iOS

    guard let rtc = self.rtcEngine else {
        return
    }
    let config = AliRtcExternalAudioStreamConfig()
    config.sampleRate = self.pcmSampleRate //PCM采样率
    config.channels = self.pcmChannels     //PCM声道数
    config.playoutVolume = 100
    config.publishVolume = 0  //只播放,不推流
    self.streamId = rtc.addExternalAudioStream(config) 
    if self.streamId <= 0 {
        debugPrint("addExternalAudioStream failed: \(self.streamId)")
        return
    }
    
  2. SDK中送入音频PCM数据。

    Android

    // 单次送给SDK的播放时长,建议为20~50ms
    int duration = 30;
    // 从缓存里获取接下来要播放的pcm数据(pcm播放时长不超过duration)
    byte[] dataToSend = ...
    // 检查dataToSend:如果未停止播放且数据太少可以sleep一下并返回等待更多缓存数据
    
    // 计算dataToSend的采样点数
    int numOfSamples = dataToSend的字节长度 / (2 * frameInfo.audio_channels);
    
    
    AliRtcEngine.AliRtcAudioFrame rawData = new AliRtcEngine.AliRtcAudioFrame();
    rawData.data = dataToSend;
    rawData.numSamples = numOfSamples;
    rawData.bytesPerSample = 2;
    rawData.numChannels = mPcmChannels;
    rawData.samplesPerSec = mPcmSampleRate;
    int ret = mAliRtcEngine.pushExternalAudioStreamRawData(mStreamId, rawData);
    if(ret == 0x01070101) {
        // rtc buffer已经满,dataToSend需要在下次再送给rtc
    } 
    else if(ret < 0) {
        // rtc播放失败,不建议继续送给rtc了,检查参数和推流状态,
        return;
    }
    else {
        // 成功送给rtc,当前的dataToSend需要从缓存里移除,下次不能再送进去
    }
    sleep(duration - 10);

    iOS

    guard let rtc = self.rtcEngine, self.streamId > 0 else {
        return
    }
    
    // 单次送给SDK的播放时长,建议为20~50ms
    let duration = 30
    // 从缓存里获取接下来要播放的pcm数据(pcm播放时长不超过duration)
    let dataToSend = ...
    // 检查dataToSend:如果未停止播放且数据太少可以sleep一下并返回等待更多缓存数据
    
    // 计算dataToSend的采样点数
    let numOfSamples = dataToSend的字节长度 / (2 * frameInfo.audio_channels)
    
    let pushFrame = AliRtcAudioFrame()
    pushFrame.dataPtr = dataToSend
    pushFrame.numOfSamples = numOfSamples  // 采样点数
    pushFrame.bytesPerSample = 2
    pushFrame.samplesPerSec = self.pcmSampleRate
    pushFrame.numOfChannels = self.pcmChannels
    let result = rtc.pushExternalAudioStream(self.pushStreamId, rawData: pushFrame)
    if result == 0x01070101 {
        // rtc buffer已经满,dataToSend需要在下次再送给rtc
    }
    else if result < 0 {
        // rtc播放失败,不建议继续送给rtc了,检查参数和推流状态,
        return
    }
    else {
        // 成功送给rtc,当前的dataToSend需要从缓存里移除,下次不能再送进去
    }
    Thread.sleep(forTimeInterval: Double(duration - 10) / 1000.0)
  3. 移除外部音频流。

    Android

    mRtcEngine.removeExternalAudioStream(mStreamId);
    
    mRtcEngine = null;
    mStreamId = -1;

    iOS

    self.rtcEngine?.removeExternalAudioStream(self.streamId)
    
    self.rtcEngine = nil
    self.streamId = -1

场景二: 播放本地音频文件

该步骤适用于通过本地音频文件播放的场景。

  1. 启动播放。

    Android

    // 音频文件路径
    private String mFilepath = "/assets/music.wav";
    
    // 播放配置
    AliRtcEngine.AliRtcAudioAccompanyConfig config = new AliRtcEngine.AliRtcAudioAccompanyConfig();
    config.loopCycles = 1;     // 循环次数
    config.publishVolume = 0;   // 只播放,不推流
    config.playoutVolume = 100;
    config.startPosMs = 0;      // 起始播放位置
    // 开始播放
    mRtcEngine.startAudioAccompany(mFilepath, config);

    iOS

    // 音频文件路径
    let filePath = Bundle.main.path(forResource: "music", ofType: "wav")
    
    // 播放配置
    let config = AliRtcAudioAccompanyConfig()
    config.loopCycles = 1   // 循环次数, -1表示无限循环
    config.publishVolume = 0   // 只播放,不推流
    config.playoutVolume = 100
    config.startPosMs = 0
    // 开始播放
    let result = rtcEngine.startAudioAccompany(withFile: filePath, config: config)
    
    说明

    在开启播放后,可以通过其他API进行播放控制,包括:

    • pauseAudioAccompany:暂停播放。

    • resumeAudioAccompany: 恢复播放。

    • stopAudioAccompany:停止播放。

  2. 监听播放完成回调。

    Android

    @Override
    public void onAudioAccompanyStateChanged(ARTCAICallEngine.ARTCAICallAudioAccompanyStateCode
                                                     playState, ARTCAICallEngine.ARTCAICallAudioAccompanyErrorCode
                                                     errorCode) {
        if (playState == ARTCAICallEngine.ARTCAICallAudioAccompanyStateCode.ARTCAICallAudioAccompanyEnded) {
            // 处理播放结束
        }
    }

    iOS

    func onAudioAccompanyStateChanged(state: ARTCAICallAudioAccompanyState, errorCode: ARTCAICallAudioAccompanyErrorCode) {
        if state == .ARTCAICallAudioAccompanyEnded {
            // 处理播放结束
        }
    }