自定义音频采集

ARTC SDK提供了灵活的自定义音频采集功能。

功能介绍

ARTC SDK 内部音频模块可满足您在应用中对基本音频功能的需求,但是在特定场景中,SDK 内部的音频采集模块可能无法满足开发需求,需要实现自定义音频采集功能,例如:

  • 解决音频采集设备被占用问题。

  • 开发者需要从定制的采集系统、音频文件中获取音频数据后交给 SDK 传输。

ARTC SDK 支持灵活的自定义采集功能,允许用户根据业务场景自行管理音频设备与音频源。

示例代码

Android端自定义音频采集Android/ARTCExample/AdvancedUsage/src/main/java/com/aliyun/artc/api/advancedusage/CustomAudioCaptureAndRender/CustomAudioCaptureActivity.java

iOS端自定义音频采集iOS/ARTCExample/AdvancedUsage/CustomAudioCapture/CustomAudioCaptureVC.swift

前提条件

在设置视频配置之前,请确保达成以下条件:

功能实现

image

1.打开或关闭内部采集

当您需要使用 SDK 的自定义音频采集功能时,通常需要关闭 SDK 内部音频采集,推荐在调用getInstance创建引擎时传入 extras 参数来关闭 SDK 内部采集,相关参数如下:

user_specified_use_external_audio_record:表示是否使用外部采集(关闭 SDK 内部采集)。

  • "TRUE":使用外部音频采集,即关闭 SDK 内部采集

  • "FALSE":不使用外部音频采集,即开启 SDK 内部采集。

说明

extras 为一个 JSON 字符串。

Android

String extras = "{\"user_specified_use_external_audio_record\":\"TRUE\"}";
mAliRtcEngine = AliRtcEngine.getInstance(this, extras);

iOS

var customVideoRenderConfig: [String: String] = [:]
// 自定义视频渲染开关
customVideoRenderConfig["user_specified_use_external_audio_record"] = "TRUE"
// 序列化为Json
guard let jsonData = try? JSONSerialization.data(withJSONObject: customVideoRenderConfig, options: []),
let extras = String(data: jsonData, encoding: .utf8) else {
    print("JSON 序列化失败")
    return
}
let engine = AliRtcEngine.sharedInstance(self, extras: extras)

Windows

/* Windows支持创建时指定开启/关闭音频采集 */
/* 关闭阿里内部采集 */
char* extra = "{\"user_specified_enable_use_virtual_audio_device\":\"TRUE\", \"user_specified_use_external_audio_record\":\"TRUE\"}";
mAliRtcEngine = AliRtcEngine.Create(extra);

/* 开启阿里内部采集 */
char* extra = "{\"user_specified_enable_use_virtual_audio_device\":\"FALSE\", \"user_specified_use_external_audio_record\":\"FALSE\"}";
mAliRtcEngine = AliRtcEngine.Create(extra);

2.添加外部音频流

调用addExternalAudioStream接口添加外部音频流,并获取音频流 ID。如果需要音频 3A(回声消除 AEC、自动增益控制 AGC、噪声抑制 ANS),请配置AliRtcExternalAudioStreamConfig中的 enable3A 参数。

说明

接口调用时机:

  • 如果需要使用 3A 的场景,推荐在音频推流成功且自定义采集模块获取到第一帧音频后调用addExternalAudioStream添加,即onAudioPublishStateChanged接口返回newStateAliRtcStatsPublished(3)之后添加。

  • 如果不需要经过 3A 的场景例如传输本地文件、网络文件、tts 生成的音频数据等,可以在创建引擎后添加,然后在音频推流成功后开始推送音频数据。

Android

AliRtcEngine.AliRtcExternalAudioStreamConfig config = new AliRtcEngine.AliRtcExternalAudioStreamConfig();
config.sampleRate = SAMPLE_RATE; // 采样率
config.channels = CHANNEL; // 通道数
// 推流音量
config.publishVolume = 100;
// 本地播放音量
config.playoutVolume = isLocalPlayout ? 100 : 0;
config.enable3A = true;

int result = mAliRtcEngine.addExternalAudioStream(config);
if (result <= 0) {
    return;
}
// 返回值为streamid,后续向SDK内push数据需要用到
mExternalAudioStreamId = result;

iOS

/* 根据自己的业务设置对应的参数 */
AliRtcExternalAudioStreamConfig *config = [AliRtcExternalAudioStreamConfig new];
//需要和外部PCM音频流的声道数相同,如果是单声道设置成1,双声道设置成2
config.channels = _pcmChannels;
//需要和外部PCM音频流的采样率相同
config.sampleRate = _pcmSampleRate;
config.playoutVolume = 0;
config.publishVolume = 100;
_externalPlayoutStreamId = [self.engine addExternalAudioStream:config];

Windows

/* 获取媒体引擎 */
IAliEngineMediaEngine* mAliRtcMediaEngine = nullptr;
    
mAliRtcEngine->QueryInterface(AliEngineInterfaceMediaEngine, (void **)&mAliRtcMediaEngine);
/* 根据自己的业务设置对应的参数 */
AliEngineExternalAudioStreamConfig config;
config.playoutVolume = currentAudioPlayoutVolume;
config.publishVolume = currentAudioPublishVolume;
config.channels = 1;
config.sampleRate = 48000;
config.publishStream = 0;
audioStreamID = mAliRtcMediaEngine->AddExternalAudioStream(config);

mAliRtcMediaEngine->Release();

3.实现音视频自采集模块

自定义采集功能需要根据您的业务场景自行采集并处理音频数据,之后将数据传入 SDK 进行传输。

阿里云提供了示例代码,演示从本地 PCM 文件或者麦克风读取 PCM 格式的数据,相关实现请参考自采集示例

4.通过外部音频流 ID 推送音频数据到 SDK

在音频推流成功后(onAudioPublishStateChanged 回调状态变为AliRtcStatsPublished),调用pushExternalAudioStreamRawData接口,将mExternalAudioStreamId参数设置为步骤 2 获取的音频流 ID, 将采集到的音频数据传入 SDK。

说明
  • 需要在音频推流成功(回调onAudioPublishStateChanged 状态为AliRtcStatsPublished )后再开始送入数据。

  • 需要按照数据的实际长度设置 AliRtcAudioFramenumSamples。部分设备上例如通过 AudioRecord.read 获取的音频数据长度可能小于传入的 buffer,需要根据返回值确定真实的音频数据长度。

  • 调用pushExternalAudioStreamRawData传入数据时,可能出现内部缓冲区满而导致失败的情形,需要进行处理并重传。

  • 通常每隔 10ms 调用pushExternalAudioStreamRawData送入一次数据。

Android

// 假设您采集到的音频数据位于audioData中,数据大小为bytesRead 字节,为10ms数据
if (mAliRtcEngine != null && bytesRead > 0) {
    // 构造AliRtcAudioFrame对象
    AliRtcEngine.AliRtcAudioFrame sample = new AliRtcEngine.AliRtcAudioFrame();
    sample.data = audioData;
    sample.numSamples = bytesRead / (channels * (bitsPerSample / 8)); // 根据实际读取的字节数计算样本数
    sample.numChannels = channels;
    sample.sampleRate = sampleRate;
    sample.bytesPerSample = bitsPerSample / 8;

    int ret = 0;
    // 当缓冲区满导致push失败的时候需要进行重试
    int retryCount = 0;
    final int MAX_RETRY_COUNT = 20;
    final int BUFFER_WAIT_MS = 10;
    do {
        // 将获取的数据送入SDK
        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);
}

iOS

// 将采集到的音频数据构造为AliRtcAudioFrame对象
let sample = AliRtcAudioFrame()
sample.dataPtr = UnsafeMutableRawPointer(mutating: pcmData)
sample.samplesPerSec = pcmSampleRate
sample.bytesPerSample = Int32(MemoryLayout<Int16>.size)
sample.numOfChannels = pcmChannels
sample.numOfSamples = numOfSamples

var retryCount = 0

while retryCount < 20 {
    if !(pcmInputThread?.isExecuting ?? false) {
        break
    }
    // 推送音频数据到SDK
    let rc = rtcEngine?.pushExternalAudioStream(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
    }
}

Windows

说明

Windows 端在实现自定义采集功能之前,需要调用QueryInterface接口获取媒体引擎对象。

/* 获取媒体引擎 */
IAliEngineMediaEngine* mAliRtcMediaEngine = nullptr;   
mAliRtcEngine->QueryInterface(AliEngineInterfaceMediaEngine, (void **)&mAliRtcMediaEngine);

// 将数据构造为音频帧
AliEngineAudioRawData rawData;
rawData.dataPtr = frameInfo.audio_data[0];
rawData.numOfSamples = (int) (frameInfo.audio_data[0].length / (2 * frameInfo.audio_channels));
rawData.bytesPerSample = 2;
rawData.numOfChannels = frameInfo.audio_channels;
rawData.samplesPerSec = frameInfo.audio_sample_rate;
// 推送数据到SDK
int ret = mAliRtcMediaEngine->PushExternalAudioStreamRawData(audioStreamID, rawData);
// 处理缓冲区满等错误

// 释放媒体引擎
mAliRtcMediaEngine->Release();

5.移除外部音频流

如果您需要停止发布自定义音频采集的音频,调用removeExternalAudioStream接口移除外部音频流。

Android

mAliRtcEngine.removeExternalAudioStream(mExternalAudioStreamId);

iOS

[self.engine removeExternalAudioStream:_externalPublishStreamId];

Windows

/* 获取媒体引擎 */
IAliEngineMediaEngine* mAliRtcMediaEngine = nullptr;
    
mAliRtcEngine->QueryInterface(AliEngineInterfaceMediaEngine, (void **)&mAliRtcMediaEngine);

mAliRtcMediaEngine->RemoveExternalAudioStream(audioStreamID);
mAliRtcMediaEngine->Release();

常见问题

  • 调用自定义音频采集pushExternalAudioStream的频率怎么设置?

    • 推荐按照物理音频设备的时钟驱动,在物理设备采集到数据的时候调用;

    • 如果没有具体的物理设备来驱动,建议 10~50ms 传入一次。

  • 使用自定义音频采集是否可以使用 SDK 内部的音频 3A(回声消除 AEC、自动增益控制 AGC、噪声抑制 ANS)?

    • 可以,第 2 步添加外部音频流时可以设置 enable3A 参数决定是否开启 SDK 内部的音频 3A。