本文介紹如何在即時互動中,將外部音頻(如背景音樂、音效或自訂的 PCM 音頻流)混入 RTC SDK 的音頻流中,實現本地播放和遠端分享。
功能介紹
ARTC SDK支援將外部音頻輸入進行本地播放和推流,相容 MP4、WAV、AAC 等多種音頻檔案格式,也支援 PCM 格式的流式音頻資料輸入。您可以根據具體的應用情境選擇最適合的音頻源,無論是預錄製好的檔案還是即時產生的資料流,都能被高效地整合與傳輸。
應用情境
推流音頻檔案:適用於直播中動態插入音效、背景音樂或口播素材,例如:電商直播中即時觸發商品提示音、遊戲直播中載入環境音效等。
推流PCM流:面向即時互動型Voice Messaging Service,例如:智能客服系統輸出的文本轉語音(TTS)產生的PCM音頻流。
範例程式碼
Android端範例程式碼:Android/ARTCExample/BasicUsage/src/main/java/com/aliyun/artc/api/basicusage/PlayAudioFiles/PlayAudioFilesActivity.java
iOS端範例程式碼:iOS/ARTCExample/BasicUsage/PlayAudioFiles/PlayAudioFilesVC.swift
Harmony端播放與推流外部輸入音頻:Harmony/ARTCExample/entry/src/main/ets/pages/basicusage/PlayAudioFilesPage.ets。
前提條件
在實現相關功能前,請確保滿足以下條件:
播放或推流音頻檔案
實現原理
該功能適用於需要播放或推流音樂檔案的多種情境。
伴奏 API:用於播放比較長的音樂檔案,例如伴奏音樂、背景音樂等。同時只能播放一個音樂檔案。
音效 API:用於播放較短的音效檔案,例如掌聲、笑聲等音效。可以同時播放多個音樂檔案。
需要加入頻道並開啟音頻推流,等到onAudioPublishStateChanged為已推流狀態後才可以播放/推流伴奏或音效檔案。
相關功能如下:
功能 | 伴奏 API | 音效 API |
音樂檔案播放、停止、暫停、恢複 |
|
|
擷取和調整播放進度 |
| |
擷取和調整音頻播放的音量 |
|
|
報告音樂檔案的播放狀態 | 本地:
遠端:
| 本地:
|
擷取音頻檔案資訊 |
| |
實現播放伴奏
調用伴奏相關 API 時,一次只能播放一個音頻檔案。
加入頻道並推送音頻流,SDK 預設是推送的。
Android
// 播放伴奏或音效需要推送音頻流,SDK預設是開啟的 mAliRtcEngine.publishLocalAudioStream(true);iOS
// 播放伴奏或音效需要推送音頻流,SDK預設是開啟的 engine.publishLocalAudioStream(true)Harmony
// 發布本地音視頻流,預設為true this.rtcEngine.publishLocalAudioStream(true);播放控制
加入頻道並推音頻流後,調用 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 的管理需您根據業務情境自行實現。
加入頻道並推送音頻流,SDK 預設是推送的。
Android
// 播放伴奏或音效需要推送音頻流,SDK預設是開啟的 mAliRtcEngine.publishLocalAudioStream(true);iOS
// 播放伴奏或音效需要推送音頻流,SDK預設是開啟的 engine.publishLocalAudioStream(true)Harmony
// 發布本地音視頻流,預設為true this.rtcEngine.publishLocalAudioStream(true);(可選)預先載入資源
如果需要重複播放同一音效,建議調用 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);播放控制
加入頻道並推音頻流後,調用 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 格式,可以參考自訂音頻採集中的方法。
實現原理
範例程式碼
添加外部輸入資料流
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];常見問題
使用
startAudioAccompany播放伴奏時,如何只讓遠端聽到伴奏聲,而聽不到本地麥克風的人聲?在開始播放伴奏前或播放過程中,調用
muteLocalMic方法即可靜音麥克風。此時,遠端只能聽到伴奏的音頻。