播放与推流外部输入音频(包括音效、伴奏)

本文介绍如何在实时互动中,将外部音频(如背景音乐、音效或自定义的 PCM 音频流)混入 RTC SDK 的音频流中,实现本地播放和远端分享。

功能介绍

ARTC SDK支持将外部音频输入进行本地播放和推流,兼容 MP4、WAV、AAC 等多种音频文件格式,也支持 PCM 格式的流式音频数据输入。您可以根据具体的应用场景选择最适合的音频源,无论是预录制好的文件还是实时生成的数据流,都能被高效地集成与传输。

应用场景

  • 推流音频文件:适用于直播中动态插入音效、背景音乐或口播素材,例如:电商直播中实时触发商品提示音、游戏直播中加载环境音效等。

  • 推流PCM流:面向实时交互型语音服务,例如:智能客服系统输出的文本转语音(TTS)生成的PCM音频流。

示例代码

Android端示例代码Android/ARTCExample/BasicUsage/src/main/java/com/aliyun/artc/api/basicusage/PlayAudioFiles/PlayAudioFilesActivity.java

iOS端示例代码iOS/ARTCExample/BasicUsage/PlayAudioFiles/PlayAudioFilesVC.swift

前提条件

在实现相关功能前,请确保满足以下条件:

播放或推流音频文件

实现原理

该功能适用于需要播放或推流音乐文件的多种场景。

  • 伴奏 API:用于播放比较长的音乐文件,例如伴奏音乐、背景音乐等。同时只能播放一个音乐文件。

  • 音效 API:用于播放较短的音效文件,例如掌声、笑声等音效。可以同时播放多个音乐文件。

说明

需要加入频道并开启音频推流,等到onAudioPublishStateChanged为已推流状态后才可以播放/推流伴奏或音效文件。

相关功能如下:

功能

伴奏 API

音效 API

音乐文件播放、停止、暂停、恢复

  • startAudioAccompany

  • stopAudioAccompany

  • pauseAudioAccompany

  • resumeAudioAccompany

  • preloadAudioEffect

  • unloadAudioEffect

  • playAudioEffect

  • stopAudioEffect

  • stopAllAudioEffects

  • pauseAudioEffect

  • pauseAllAudioEffects

  • resumeAudioEffect

  • resumeAllAudioEffects

获取和调整播放进度

  • getAudioAccompanyDuration

  • getAudioAccompanyCurrentPosition

  • setAudioAccompanyPosition


获取和调整音频播放的音量

  • setAudioAccompanyVolume

  • setAudioAccompanyPublishVolume

  • getAudioAccompanyPublishVolume

  • setAudioAccompanyPlayoutVolume

  • getAudioAccompanyPlayoutVolume

  • setAudioEffectPublishVolume

  • getAudioEffectPublishVolume

  • setAudioEffectPlayoutVolume

  • getAudioEffectPlayoutVolume

  • setAllAudioEffectsPublishVolume

  • setAllAudioEffectsPlayoutVolume

报告音乐文件的播放状态

本地:

  • onAudioAccompanyStateChanged

远端:

  • onRemoteAudioAccompanyStarted

  • onRemoteAudioAccompanyFinished

本地:

  • onAudioEffectFinished

获取音频文件信息

  • getAudioFileInfo

  • onAudioFileInfo

实现播放伴奏

调用伴奏相关 API 时,一次只能播放一个音频文件。

  1. 加入频道并推送音频流,SDK 默认是推送的。

    Android

    // 播放伴奏或音效需要推送音频流,SDK默认是开启的
    mAliRtcEngine.publishLocalAudioStream(true);

    iOS

    // 播放伴奏或音效需要推送音频流,SDK默认是开启的
    engine.publishLocalAudioStream(true)

    Harmony

    // 发布本地音视频流,默认为true
    this.rtcEngine.publishLocalAudioStream(true);
  2. 播放控制

加入频道并推音频流后,调用 startAudioAccompany 播放伴奏。成功调用该方法后,本地会触发 onAudioAccompanyStateChanged 回调,远端会触发 onRemoteAudioAccompanyStarted 回调。

除此之外,你还可以通过下面的接口实现播放控制:

  • stopAudioAccompany:停止播放。

  • pauseAudioAccompany:暂停播放。

  • resumeAudioAccompany:恢复播放。

  • getAudioAccompanyDuration、getAudioAccompanyCurrentPosition、setAudioAccompanyPosition:文件时长获取与播放进度控制。

  • setAudioAccompanyVolume:同时设置伴奏本地播放和远端推流的音量。

  • getAudioAccompanyPublishVolume、setAudioAccompanyPublishVolume:获取或调节伴奏在远端的播放音量。

  • getAudioAccompanyPlayoutVolume、setAudioAccompanyPlayoutVolume:获取或调节伴奏在本地播放的音量。

Android

// 伴奏文件路径
private String mMixingMusicFilepath = "/assets/music.wav";

// 伴奏播放配置
AliRtcEngine.AliRtcAudioAccompanyConfig config = new AliRtcEngine.AliRtcAudioAccompanyConfig();
config.loopCycles = -1; // 循环次数, -1表示无限循环
config.publishVolume = publishVolume; // 推流音量,[0-100]
config.playoutVolume = playbackVolume; // 本地播放音量,[0-100]
config.startPosMs = 0; // 起始播放位置
// 开始播放
mAliRtcEngine.startAudioAccompany(mMixingMusicFilepath, config);

// 暂停/恢复伴奏
mAliRtcEngine.pauseAudioAccompany();
mAliRtcEngine.resumeAudioAccompany();

// 停止播放
mAliRtcEngine.stopAudioAccompany();

// 获取伴奏文件的时长(单位为ms),需要在startAudioAccompany之后调用,否则会返回-1
int duration = mAliRtcEngine.getAudioAccompanyDuration();
// 如果需要获取指定文件的时长,可以通过getAudioFileInfo接口,该接口在创建引擎后即可调用,结果通过onAudioFileInfo回调返回
mAliRtcEngine.getAudioFileInfo(filePath);
@Override
public void onAudioFileInfo(AliRtcEngine.AliRtcAudioFileInfo info, AliRtcEngine.AliRtcAudioAccompanyErrorCode errorCode) {

    handler.post(() -> {
        String msg = "onAudioFileInfo.file:" + info.filePath + "duration:" + info.durationMs + "audioPlayingErrorCode=" + errorCode;
        ToastHelper.showToast(PlayAudioFilesActivity.this, msg, Toast.LENGTH_SHORT);
    });
}

// 获取/设置音量
mAliRtcEngine.setAudioAccompanyVolume(50); // [0-100]
int publishVolume = mAliRtcEngine.getAudioAccompanyPublishVolume();
mAliRtcEngine.setAudioAccompanyPublishVolume(50);
int playoutVolume = mAliRtcEngine.getAudioAccompanyPlayoutVolume();
mAliRtcEngine.setAudioAccompanyPlayoutVolume(50);

// 获取/设置播放进度
int currPosition = mAliRtcEngine.getAudioAccompanyCurrentPosition(); // 单位为ms
int targetPosition = 1000; // 以1000ms处开始播放为例
mAliRtcEngine.setAudioAccompanyPosition(targetPosition);

iOS

// 伴奏文件路径
let filePath = Bundle.main.path(forResource: "music", ofType: "wav")

// 伴奏播放配置
let config = AliRtcAudioAccompanyConfig()
config.loopCycles = loopCount
config.publishVolume = publishVolume
config.playoutVolume = playoutVolume
config.startPosMs = 0
// 开始播放
let result = rtcEngine.startAudioAccompany(withFile: filePath, config: config)

// 暂停/恢复播放
rtcEngine.pauseAudioAccompany()
rtcEngine.resumeAudioAccompany()

// 停止播放
rtcEngine.stopAudioAccompany()

// 获取伴奏文件的时长(单位为ms),需要在startAudioAccompany之后调用,否则会返回-1
let duration = rtcEngine.getAudioAccompanyDuration()
// 如果需要获取指定文件的时长,可以通过getAudioFileInfo接口,该接口在创建引擎后即可调用,结果通过onAudioFileInfo回调返回
rtcEngine.getAudioFileInfo(filePath)
func onAudioFileInfo(_ info: AliRtcAudioFileInfo, errorCode: AliRtcAudioAccompanyErrorCode) {
    "onAudioFileInfo, filePath: \(info.filePath), durationMs: \(info.durationMs), errorCode: \(errorCode)".printLog()
}

// 获取/设置音量
rtcEngine.setAudioAccompanyVolume(50) // 同时设置伴奏推流和播放音量,范围[0-100]
let audioPublishVolume = rtcEngine.getAudioAccompanyPublishVolume() // 获取当前伴奏的推流音量
rtcEngine.setAudioAccompanyPublishVolume(50) // 设置伴奏推流音量,范围[0-100]
let audioPlayoutVolume = rtcEngine.getAudioAccompanyPlayoutVolume() // 获取当前伴奏的本地播放音量
rtcEngine.setAudioAccompanyPlayoutVolume(50) // 设置伴奏的本地播放音,范围[0-100]

// 获取/设置播放进度
let currentPosition = rtcEngine.getAudioAccompanyCurrentPosition() // 单位为ms
let targetPosition:Int32 = 1000; // 以1000ms处开始播放为例
rtcEngine.setAudioAccompanyPosition(targetPosition)

Harmony

// 伴奏文件路径
private accompanimentFile: string = 'music.wav';
// 开始伴奏
private async startAudioAccompany(): Promise<void> {
  if (!this.rtcEngine) {
    prompt.showToast({ message: 'RTC引擎未初始化', duration: 2000 });
    return;
  }
  if (!this.hasJoined) {
    prompt.showToast({ message: '请先加入频道', duration: 2000 });
    return;
  }
  try {
    // 获取循环次数
    const loopCount = this.loopCount.trim() === '' ? -1 : parseInt(this.loopCount);
    const publishVolume = this.pushVolume;  // 推流音量
    const playbackVolume = this.playVolume; // 本地播放音量
    // 创建伴奏配置
    const config: AliRtcAudioAccompanyConfig = new AliRtcAudioAccompanyConfig();
    config.loopCycles = loopCount;      // 循环次数,-1表示无限循环
    config.publishVolume = publishVolume; // 推流音量 [0-100]
    config.playoutVolume = playbackVolume; // 本地播放音量 [0-100]
    config.startPosMs = 0;              // 起始播放位置
    // 使用沙箱文件路径
    const audioPath = this.sandboxManager.getSandboxFilePath(this.accompanimentFile);
    // 开始播放
    this.rtcEngine.startAudioAccompany(audioPath, config);
    // 获取音频时长并更新进度条
    const audioDuration = this.rtcEngine.getAudioAccompanyDuration(); // 单位为ms
    if (audioDuration > 0) {
      this.audioDuration = audioDuration;
    }
    console.info('开始播放伴奏');
  } catch (error) {
    console.error('播放伴奏异常:', error);
    prompt.showToast({ message: '播放伴奏异常', duration: 2000 });
  }
}
// 暂停伴奏
private pauseAudioAccompany(): void {
  if (!this.rtcEngine) {
    prompt.showToast({ message: 'RTC引擎未初始化', duration: 2000 });
    return;
  }
  this.rtcEngine.pauseAudioAccompany();
  console.info('暂停播放伴奏');
}
// 恢复伴奏
private resumeAudioAccompany(): void {
  if (!this.rtcEngine) {
    prompt.showToast({ message: 'RTC引擎未初始化', duration: 2000 });
    return;
  }
  this.rtcEngine.resumeAudioAccompany();
  console.info('恢复播放伴奏');
}
// 停止伴奏
private stopAudioAccompany(): void {
  if (!this.rtcEngine) {
    prompt.showToast({ message: 'RTC引擎未初始化', duration: 2000 });
    return;
  }
  this.rtcEngine.stopAudioAccompany();
  console.info('停止播放伴奏');
}
// 设置伴奏音量
private setAudioAccompanyVolume(volume: number): void {
  if (this.rtcEngine && this.hasJoined) {
    this.rtcEngine.setAudioAccompanyVolume(volume); // [0-100]
  }
}
// 设置推流音量
private setAudioAccompanyPublishVolume(volume: number): void {
  if (this.rtcEngine && this.hasJoined) {
    this.rtcEngine.setAudioAccompanyPublishVolume(volume); // [0-100]
  }
}
// 设置播放音量
private setAudioAccompanyPlayoutVolume(volume: number): void {
  if (this.rtcEngine && this.hasJoined) {
    this.rtcEngine.setAudioAccompanyPlayOutVolume(volume); // [0-100]
  }
}
// 获取推流音量
private getAudioAccompanyPublishVolume(): number {
  if (this.rtcEngine) {
    return this.rtcEngine.getAudioAccompanyPublishVolume();
  }
  return 0;
}
// 获取播放音量
private getAudioAccompanyPlayoutVolume(): number {
  if (this.rtcEngine) {
    return this.rtcEngine.getAudioAccompanyPlayoutVolume();
  }
  return 0;
}
// 设置播放位置
private setAudioAccompanyPosition(position: number): void {
  if (this.rtcEngine && this.hasJoined) {
    if (position >= 0 && position <= this.audioDuration) {
      this.rtcEngine.setAudioAccompanyPosition(position); // 单位为ms
    }
  }
}
// 获取当前播放位置
private getAudioAccompanyCurrentPosition(): number {
  if (this.rtcEngine && this.hasJoined) {
    return this.rtcEngine.getAudioAccompanyCurrentPosition(); // 单位为ms
  }
  return 0;
}
// 更新播放进度的定时器示例
private updatePlayProgress(): void {
  if (this.rtcEngine && this.hasJoined) {
    const currentPosition = this.rtcEngine.getAudioAccompanyCurrentPosition();
    this.playProgress = currentPosition;
  }
}

实现播放音效

如需同时播放多个音效文件(例如掌声、笑声),可调用 preloadAudioEffect 和 playAudioEffect 等 API。

说明

每个音效文件需要分配一个唯一的 ID。ID 的管理需您根据业务场景自行实现。

  1. 加入频道并推送音频流,SDK 默认是推送的。

    Android

    // 播放伴奏或音效需要推送音频流,SDK默认是开启的
    mAliRtcEngine.publishLocalAudioStream(true);

    iOS

    // 播放伴奏或音效需要推送音频流,SDK默认是开启的
    engine.publishLocalAudioStream(true)

    Harmony

    // 发布本地音视频流,默认为true
    this.rtcEngine.publishLocalAudioStream(true);
  2. (可选)预加载资源

如果需要重复播放同一音效,建议调用 preloadAudioEffect 将音频文件预加载到内存,以优化性能。

  • preloadAudioEffect:加载指定的音效文件,支持本地文件和网络文件 url,推荐使用本地文件。

  • unloadAudioEffect:当加载的音效文件使用完毕后,可以调用此接口卸载以释放相关资源。

Android

// 加载指定ID和filePath的音效文件,ID请自行定义
mAliRtcEngine.preloadAudioEffect(mCurrSoundID, filePath);
// 卸载指定ID的音效文件
mAliRtcEngine.unloadAudioEffect(mCurrSoundID);

iOS

// 加载指定ID和filePath的音效文件,ID请自行定义
rtcEngine.preloadAudioEffect(withSoundId: self.soundId, filePath: filePath)
// 卸载指定ID的音效文件
rtcEngine.unloadAudioEffect(withSoundId: self.currentEffectIndex)

Harmony

// 加载指定ID和filePath的音效文件,ID请自行定义
this.rtcEngine.preloadAudioEffect(1, audioPath);
// 卸载指定ID的音效文件
this.rtcEngine.unloadAudioEffect(this.currSoundID);
  1. 播放控制

加入频道并推音频流后,调用 playAudioEffect 播放音效文件。播放完成后,本地会触发 onAudioEffectFinished 回调。

除此之外,你还可以通过下面的接口实现播放控制:

  • stopAudioEffect/stopAllAudioEffects:停止播放。

  • pauseAudioEffect/pauseAllAudioEffects:暂停播放。

  • resumeAudioEffect/resumeAllAudioEffects:恢复播放。

  • getAudioEffectPublishVolume、setAudioEffectPublishVolume、setAllAudioEffectsPublishVolume:获取或调节音效在远端的播放音量。

  • getAudioEffectPlayoutVolume、setAudioEffectPlayoutVolume、setAllAudioEffectsPlayoutVolume:获取或调节音效在本地播放的音量。

Android

// 播放伴奏
AliRtcEngine.AliRtcAudioEffectConfig config = new AliRtcEngine.AliRtcAudioEffectConfig();
config.loopCycles = -1;			// 循环播放次数,-1表示无限循环
config.startPosMs = 0;			// 开始播放位置
config.publishVolume = 50;		// 推流音量,[0-100]
config.playoutVolume = 50;		// 本地播放音量,[0-100]
mAliRtcEngine.playAudioEffect(mCurrSoundID, filePath, config);

// 停止播放
mAliRtcEngine.stopAudioEffect(soundId);
mAliRtcEngine.stopAllAudioEffects();

// 暂停播放
mAliRtcEngine.pauseAudioEffect(soundId);
mAliRtcEngine.pauseAllAudioEffects();

// 恢复播放
mAliRtcEngine.resumeAudioEffect(soundId);
mAliRtcEngine.resumeAllAudioEffects();

// 获取或者设置音量
// 推流音量
mAliRtcEngine.getAudioEffectPublishVolume(soundId);
mAliRtcEngine.setAudioEffectPublishVolume(soundId, 50);
mAliRtcEngine.setAllAudioEffectsPublishVolume(50);
// 播放音量
mAliRtcEngine.getAudioEffectPlayoutVolume(soundId);
mAliRtcEngine.setAudioEffectPlayoutVolume(soundId, 50);
mAliRtcEngine.setAllAudioEffectsPlayoutVolume(50);

iOS

// 播放
let config = AliRtcAudioEffectConfig()
config.loopCycles = -1
config.startPosMs = 0
config.publishVolume = 50
config.playoutVolume = 50
let result = rtcEngine.playAudioEffect(withSoundId: self.soundId, filePath: filePath, config: config)
if result != 0 {
    UIAlertController.showAlertWithMainThread(msg: "播放音效失败,错误码: \(result)", vc: self)
}

// 停止播放
rtcEngine.stopAudioEffect(withSoundId: soundId)
rtcEngine.stopAllAudioEffects()

// 暂停播放
rtcEngine.pauseAudioEffect(withSoundId: soundId)
rtcEngine.pauseAllAudioEffects()

// 恢复播放
rtcEngine.resumeAudioEffect(withSoundId: soundId)
rtcEngine.resumeAllAudioEffects()

// 获取或设置音量
rtcEngine.getAudioEffectPublishVolume(withSoundId: soundId)
rtcEngine.setAudioEffectPublishVolumeWithSoundId(soundId, volume: 50)
rtcEngine.setAllAudioEffectsPublishVolume(50)
rtcEngine.resumeAudioEffect(withSoundId: soundId)    
rtcEngine.getAudioEffectPlayoutVolume(withSoundId: soundId)
rtcEngine.setAudioEffectPlayoutVolumeWithSoundId(soundId, volume: 50)

Harmony

// 播放
const config: AliRtcAudioEffectConfig = new AliRtcAudioEffectConfig();
config.loopCycles = loopCount;
config.startPosMs = 0;
config.publishVolume = this.soundEffectVolume;
config.playoutVolume = this.soundEffectVolume;
const audioPath = this.sandboxManager.getSandboxFilePath(filePath);
this.rtcEngine.playAudioEffect(this.currSoundID, audioPath, config);

// 停止播放
this.rtcEngine.stopAudioEffect(soundId);
this.rtcEngine.stopAllAudioEffects();

// 暂停播放
this.rtcEngine.pauseAudioEffect(soundId);
this.rtcEngine.pauseAllAudioEffects();

// 恢复播放
this.rtcEngine.resumeAudioEffect(soundId);
this.rtcEngine.resumeAllAudioEffects();

播放或推流 PCM 音频数据

如果需要播放或推流的音频数据为 PCM 格式,可以参考自定义音频采集中的方法。

实现原理

image

示例代码

添加外部输入流

Android

/* 根据自己的业务设置对应的参数 */
AliRtcEngine.AliRtcExternalAudioStreamConfig config = new AliRtcEngine.AliRtcExternalAudioStreamConfig();
/* 设置播放音量为0 ,意味着不进行本地播放 */
config.playoutVolume = currentAudioPlayoutVolume;
/* 设置推流音量为0, 意味着不进行推流 */
config.publishVolume = currentAudioPublishVolume;
config.channels = 1;
config.sampleRate = 48000;
// 返回值为外部输入流ID,后续通过该ID将数据送入SDK
audioStreamID = mAliRtcEngine.addExternalAudioStream(config);

iOS

/* 根据自己的业务设置对应的参数 */
AliRtcExternalAudioStreamConfig *config = [AliRtcExternalAudioStreamConfig new];
config.channels = _pcmChannels;
config.sampleRate = _pcmSampleRate;
/* 设置播放音量为0 ,意味着不进行本地播放 */
config.playoutVolume = 0;
/* 设置推流音量为0, 意味着不进行推流 */
config.publishVolume = 100;
// 返回值为外部输入流ID,后续通过该ID将数据送入SDK
_externalPlayoutStreamId = [self.engine addExternalAudioStream:config];

向音频流内送入PCM数据

Android

AliRtcEngine.AliRtcAudioFrame rawData = new AliRtcEngine.AliRtcAudioFrame();
rawData.data = frameInfo.audio_data[0];
rawData.numSamples = (int) (frameInfo.audio_data[0].length / (2 * frameInfo.audio_channels));
rawData.bytesPerSample = 2;
rawData.numChannels = frameInfo.audio_channels;
rawData.samplesPerSec = frameInfo.audio_sample_rate;

int ret = mAliRtcEngine.pushExternalAudioStreamRawData(audioStreamID, rawData);
if(ret == 0x01070101) {
    // 缓冲区满
    sleep(20);
} else if(ret < 0) {
    /* 异常, 检查参数和推流状态 */
}

iOS

AliRtcAudioFrame *sample = [AliRtcAudioFrame new];
sample.dataPtr = _pcmLocalData;
sample.samplesPerSec = _pcmLocalSampleRate;
sample.bytesPerSample = sizeof(int16_t);
sample.numOfChannels = _pcmLocalChannels;
sample.numOfSamples = numOfSamples;
int rc = [self.engine pushExternalAudioStream:_externalPlayoutStreamId rawData:sample];

if(rc == 0x01070101) {
    // 缓冲区满
    sleep(20);
} else if(ret < 0) {
    /* 异常, 检查参数和推流状态 */
}

移除外部音频流

Android

mAliRtcEngine.removeExternalAudioStream(audioStreamID);

iOS

[self.engine removeExternalAudioStream:_externalPublishStreamId];

常见问题

  1. 使用 startAudioAccompany 播放伴奏时,如何只让远端听到伴奏声,而听不到本地麦克风的人声?

    在开始播放伴奏前或播放过程中,调用 muteLocalMic 方法即可静音麦克风。此时,远端只能听到伴奏的音频。