本文档介绍如何使用 AICallKit SDK,将您自行采集的音频 PCM 数据推送给 SDK,以实现自定义的音频采集功能。
功能介绍
在通话过程中,AICallKit 通常会使用默认的音频采集模块。但是受限于音频麦克风设备的差异性,当默认的音频采集没有办法满足需求时,客户可以自行实现音频采集模块,将自行采集到的PCM数据通过AICallKit SDK提供的外部音频输入接口送给SDK,实现和智能体通话。
前提条件
已经集成音视频通话智能体,并实现了基础的音视频通话功能,请参考音视频通话智能体集成。
已经实现自定义采集模块,并可以获取音频 PCM 数据,阿里云提供了示例代码,演示从本地 PCM 文件或者麦克风读取 PCM 格式的数据,相关实现请参考自采集示例。
功能实现
AICallKit SDK 未直接提供外部音频输入接口,该功能依赖其底层的 AliVCSDK_ARTC SDK 实现。您可以通过 AICallKit 获取AliRtcEngine引擎对象,并调用其 API 来实现外部音频采集和推流。交互流程如下:
当 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)