如何实现外部音频采集与推送

本文档介绍如何使用 AICallKit SDK,将您自行采集的音频 PCM 数据推送给 SDK,以实现自定义的音频采集功能。

功能介绍

在通话过程中,AICallKit 通常会使用默认的音频采集模块。但是受限于音频麦克风设备的差异性,当默认的音频采集没有办法满足需求时,客户可以自行实现音频采集模块,将自行采集到的PCM数据通过AICallKit SDK提供的外部音频输入接口送给SDK,实现和智能体通话。

前提条件

  • 已经集成音视频通话智能体,并实现了基础的音视频通话功能,请参考音视频通话智能体集成

  • 已经实现自定义采集模块,并可以获取音频 PCM 数据,阿里云提供了示例代码,演示从本地 PCM 文件或者麦克风读取 PCM 格式的数据,相关实现请参考自采集示例

功能实现

AICallKit SDK 未直接提供外部音频输入接口,该功能依赖其底层的 AliVCSDK_ARTC SDK 实现。您可以通过 AICallKit 获取AliRtcEngine引擎对象,并调用其 API 来实现外部音频采集和推流。交互流程如下:

image
说明

当 AICallKit 未直接提供您所需的功能,但其底层的 AliVCSDK_ARTC SDK 支持时,您可以通过 ARTCAICallEngine 的 getRtcEngine() 接口获取 AliRtcEngine 引擎对象,进而调用ARTC SDK的接口。

步骤一:配置关闭SDK内部音频采集

在初始化 ARTCAICallEngine 前,请设置 ARTCAICallBase.artcDefaultExtras 中的 user_specified_use_external_audio_record 字段为 "TRUE",以禁用 SDK 内置的音频采集模块。

说明

ARTCAICallBase.artcDefaultExtras 的配置必须在调用 ARTCAICallEngine 的 init 方法之前完成,否则配置无效。

Android

// artcDefaultExtras为一个静态变量,格式为JSONObject,默认值为null
ARTCAICallBase.artcDefaultExtras = new JSONObject();
try {
    // 配置关闭SDK内部采集
    ARTCAICallBase.artcDefaultExtras.put("user_specified_use_external_audio_record", "TRUE");
} catch (JSONException e) {
    e.printStackTrace();
}

iOS

// ARTCAICallBase.artcDefaultExtras为静态变量,格式为[String: Any],默认值为[:]
// 配置关闭SDK内部采集
ARTCAICallBase.artcDefaultExtras = ["user_specified_use_external_audio_record": "TRUE"]

步骤二:获取AliRtcEngine引擎对象

AICallKit 初始化其底层的 ARTC 引擎后,您可以通过以下任一方式获取 AliRtcEngine 对象:

  • (推荐)在 onAliRtcEngineCreated 回调中获取。此回调是获取引擎对象的最佳时机。

    Android

    @Override
    public void onAliRtcEngineCreated(AliRtcEngine engine) {
        if(engine != null) {
         //获取AliRtcEngine对象engine, 添加外部输入音频流
    
        }
    }

    iOS

    public func onAICallRTCEngineCreated() {
        guard let engine = self.engine.getRTCInstance() as? AliRtcEngine else {
            return
        }
        // 保存AliRtcEngine对象engine, 添加外部输入音频流
        self.rtcEngine = engine
    }
    
  • (备选)调用 ARTCAICallEngine 的 getRtcEngine() 方法。请确保在 onAliRtcEngineCreated 回调触发后再调用此方法。

步骤三:添加外部输入音频流

获取 AliRtcEngine 对象后,调用其 addExternalAudioStream 方法添加外部音频流。建议在 onAliRtcEngineCreated 回调中执行此操作。

Android

private void addExternalAudio() {
    // 根据业务场景进行配置
    AliRtcEngine.AliRtcExternalAudioStreamConfig config = new AliRtcEngine.AliRtcExternalAudioStreamConfig();
    config.sampleRate = SAMPLE_RATE;//输入的外部音频流的采样率
    config.channels = CHANNEL;//输入的外部音频流的采样率
    config.publishVolume = 100;
    config.playoutVolume =  0;
    config.enable3A = true;

    int result = mAliRtcEngine.addExternalAudioStream(config);
    if (result <= 0) {
        return;
    }
    // 返回值为streamid
    mExternalAudioStreamId = result;
}

iOS

func addExternalAudio() {
    let config = AliRtcExternalAudioStreamConfig();
    config.sampleRate = SAMPLE_RATE   //输入的外部音频流的采样率
    config.channels = CHANNEL         //输入的外部音频流的采样率
    config.playoutVolume = 100
    config.publishVolume = 0
    config.enable3A = true
    let streamId = self.rtcEngine?.addExternalAudioStream(config)
    if (streamId <= 0) {
        // failed
    }
    else {
        self.externalPublishStreamId = streamId;
    }
}

步骤四:向SDK送入PCM数据

可在 AICallKit SDK 的 onCallBegin 回调中送入音频 PCM 数据。业务层可控制送入数据的大小和频率,例如每次送入 30ms 音频数据,每 20ms 发送一次。

说明
  • 送入数据时,应根据实际音频长度正确设置 AliRtcAudioFrame 的 numSamples。在部分设备上(如通过 AudioRecord.read 获取数据),实际读取长度可能小于缓冲区容量,需依据返回值确定真实数据长度。

  • 调用 pushExternalAudioStreamRawData 送入数据时,可能因 SDK 内部缓冲区满而失败(错误码:ERR_SDK_AUDIO_INPUT_BUFFER_FULL: 0x01070101)。此时需处理错误,适当sleep后再重试,避免持续写入导致阻塞。

示例代码如下:

Android

@Override
public void onCallBegin() {
    startPushAudioRawData();
}

public void startPushAudioRawData() {
   // 1. 从AudioRecord中read数据
int bytesRead = 0;
// 根据音频源类型读取数据
bytesRead = audioRecord.read(buffer, 0, buffer.length);   
// 2. 通过pushExternalAudioStreamRawData接口向SDK内送数据
if (mAliRtcEngine != null && bytesRead > 0) {
    // 构造AliRtcAudioFrame对象
    AliRtcEngine.AliRtcAudioFrame sample = new AliRtcEngine.AliRtcAudioFrame();
    sample.data = audioData;
    sample.numSamples = bytesRead / (channels * (bitsPerSample / 8)); // 根据实际读取的字节数计算样本数
    sample.numChannels = CHANNEL;
    sample.samplesPerSec = SAMPLE_RATE; // 采样率
    sample.bytesPerSample = bitsPerSample / 8; // 每个样本多少字节
    // 将获取的数据送入SDK
    int ret = 0;
    // 当缓冲区满导致push失败的时候需要进行重试
    int retryCount = 0;
    final int MAX_RETRY_COUNT = 20;
    final int BUFFER_WAIT_MS = 10;
    do {
        ret = mAliRtcEngine.pushExternalAudioStreamRawData(mExternalAudioStreamId, sample);
        if(ret == ErrorCodeEnum.ERR_SDK_AUDIO_INPUT_BUFFER_FULL) {
            // 处理缓冲区满的情况,等待一段时间重试,最多重试几百ms
            retryCount++;
            if(mExternalAudioStreamId <= 0 || retryCount >= MAX_RETRY_COUNT) {
                // 已经停止推流或者重试次数过多,退出循环
                break;
            }

            try {
                // 暂停一段时间
                Thread.sleep(BUFFER_WAIT_MS);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        } else {
            // 推送成功或者其他错误直接退出循环
            break;
        }
    } while (retryCount < MAX_RETRY_COUNT);

    // 推送失败记录日志
    if(ret != 0) {
        if(ret == ErrorCodeEnum.ERR_SDK_AUDIO_INPUT_BUFFER_FULL) {
            // 如果重试后仍然失败,记录日志
            Log.w("CustomAudioCapture", "推送音频数据失败,错误码: " + ret + ",重试次数: " + retryCount);
        } else {
            Log.e("CustomAudioCapture", "推送音频数据失败,错误码:" + ret);
        }
    }
}

iOS

let sample = AliRtcAudioFrame()
sample.dataPtr = dataPtr
sample.samplesPerSec = SAMPLE_RATE
sample.bytesPerSample = bytesPerSample
sample.numOfChannels = CHANNEL
sample.numOfSamples = numOfSamples

var retryCount = 0

while retryCount < 20 {
    if !(self.externalPublishStreamId > 0) {
        // 如果当前不需要推流了,直接跳出
        break
    }

    let rc = self.rtcEngine?.pushExternalAudioStream(self.externalPublishStreamId, rawData: sample) ?? 0

    // 0x01070101 SDK_AUDIO_INPUT_BUFFER_FULL 缓冲区满了需要重传
    if rc == 0x01070101 && !(pcmInputThread?.isCancelled ?? true) {
        Thread.sleep(forTimeInterval: 0.03) // 30ms
        retryCount += 1;
    } else {
        if rc < 0 {
            "pushExternalAudioStream error, ret: \(rc)".printLog()
        }
        break
    }
}

步骤五:移除外部音视频流

Android

engine.removeExternalAudioStream(mExternalAudioStreamId);

iOS

self.rtcEngine?.removeExternalAudioStream(self.externalPublishStreamId)