实现串行合唱
更新时间: 2025/09/22 13:44:12
串行合唱场景中,主唱将伴奏和人声发送给合唱者,合唱者根据主唱的伴奏进行合唱,并混流发给观众。
功能原理
串行合唱的原理图如下。

串行合唱的原理说明如下:
- 主唱播放伴奏,并以主流方式发送伴奏。
- 主唱的 NERTC SDK 将主唱的干声(Audio1)和伴奏合流后发给合唱者,主唱的干声(Audio1)和伴奏只有合唱者才能听到,主唱不订阅合唱者的干声。
- 合唱者将伴奏、主唱的干声(Audio1)和合唱者自己的干声(Audio2)混音后发给观众。
- 观众只听合唱者的声音。
功能介绍
NTP 实时合唱和串行合唱的方案对比如下表所示。
| 维度 | NTP 实时合唱 | 串行合唱 |
|---|---|---|
| 主唱体验 | 能实时听到伴奏和合唱者的声音 | 只能听到伴奏,不能听到合唱者的声音 |
| 合唱者体验 | 能实时听到伴奏和主唱的声音 | 听到伴奏和主唱同步 |
| 观众体验 | 能实时听到伴奏、主唱的声音、合唱者的声音,且三者同步 | 听到伴奏、主唱的声音、合唱者的声音,且三者同步 |
| 硬件要求 | 对网络和机型有一定要求 | 抗弱网性能好,机型覆盖全 |
| 实现难度 | 接入成本低 | 接入成本中等 |
前提条件
- 已实现 加入和离开 RTC 房间。
- 已调用
setChannelProfile接口,将房间场景为STANDARD_CHATROOM场景。
主唱和合唱者上麦
麦位相关的实现逻辑需要业务自行实现。
点歌
合唱
时序图
sequenceDiagram
participant 主唱
participant 副唱
participant 听众
rect rgb(240,248,255)
Note over 主唱,副唱: 进入房间
主唱->>主唱: subscribeRemoteAudioStream<br>不订阅副唱音频
主唱->>主唱: setAudioSubscribeOnlyBy<br>设置自己的音频只给合唱者
主唱->>主唱: 调用 setChannelProfile 设置房间场景为 Karaoke
副唱->>副唱: 调用 setChannelProfile 设置房间场景为 Karaoke
end
rect rgb(255,250,240)
Note over 主唱,听众: 开始唱歌
主唱->>副唱: 调用 playEffect<br>以主流方式发送伴奏
主唱->>主唱: updateAudioEffectTimestamp<br>收到伴奏进度回调
主唱->>副唱: sendSEIMsg 将自己的播放进度发出去
副唱->>听众: 转发 SEI 消息
副唱->>副唱: onRecvSEIMsg<br>收到 SEIMsg,同步歌词进度
听众->>听众: onRecvSEIMsg<br>收到 SEIMsg,同步歌词进度
副唱->>副唱: 在 onPlaybackAudioFrameBeforeMixingWithUserID<br>和 onRecordFrame 回调中 Mix 声音
end
rect rgb(240,248,255)
Note over 主唱,副唱: 结束唱歌
主唱->>主唱: 停止合唱:stopEffect<br>主唱恢复副唱音频:subscribeRemoteAudioStream<br>主唱声音恢复为所有人:setAudioSubscribeOnlyBy
副唱->>副唱: 停止合唱:stopEffect<br>订阅主播伴奏:subscribeRemoteSubStreamAudio
主唱->>主唱: 调用 setChannelProfile 设置房间场景为 STANDARD_CHATROOM
副唱->>副唱: 调用 setChannelProfile 设置房间场景为 STANDARD_CHATROOM
end
实现流程
-
主唱发起合唱邀请,具体实现方式需要业务自行实现。
-
观众同意合唱,并申请上麦。上麦成功后,调用
enableLocalAudio接口,将参数设置为true,开启本地音频采集和发送。以下示例代码展示通过 IM 实现上麦,您可以自行实现相关逻辑。
Java//申请上麦 sendNotification(SeatCommands.applySeat(voiceRoomInfo, user, seat), new RequestCallback<Void>() { @Override public void onSuccess(Void param) { if (mySeat == null) { return; } if (callback != null) { callback.onSuccess(param); } } @Override public void onFailed(int code) { if (callback != null) { callback.onFailed(code); } } @Override public void onException(Throwable exception) { if (callback != null) { callback.onException(exception); } } }); //开启本地音频采集和发送 NERtcEx.getInstance().enableLocalAudio(true);
-
合唱前,主唱和合唱者调用
setChannelProfile接口将房间场景设置为Karaoke场景。Java
NERtcEx.getInstance().setChannelProfile(NERtcConstants.RTCChannelProfile.Karaoke); -
唱歌开始前,设置主唱不订阅合唱者的声音,并且自己的声音只发给合唱者。
JavaNERtcEx.getInstance().subscribeRemoteAudioStream(chrousUid, false); //不订阅合唱者的音频 long[] uids = new long[] {chrousUid}; NERtcEx.getInstance().setAudioSubscribeOnlyBy(uids); //自己的音频只给合唱者 -
主唱调用
playEffect接口,以主流方式发送伴奏给合唱者。收到歌曲开始消息后,开始 3 秒倒计时,3 秒后开始播放伴奏。
Javalong position = System.currentTimeMillis() + 3 * 1000; // 3秒后开始播放伴奏 NERtcCreateAudioEffectOption pureOpt = new NERtcCreateAudioEffectOption(); pureOpt.path = 纯伴奏音乐路径; pureOpt.loopCount = 1; pureOpt.sendEnabled = true; pureOpt.sendVolume = 发送音量; pureOpt.playbackVolume = 播放音量; pureOpt.sendWithAudioType = NERtcAudioStreamType.kNERtcAudioStreamTypeSub; //音效以辅流的方式发送 pureOpt.startTimestamp = position; NERtcEx.getInstance().playEffect(pureEffectId, pureOpt); //自定义的纯伴奏的effectid. NERtcCreateAudioEffectOption originOpt = new NERtcCreateAudioEffectOption(); originOpt.path = 原声伴奏音乐路径; originOpt.loopCount = 1; originOpt.sendEnabled = true; originOpt.sendVolume = 0; //如果当前放纯音效音乐,将带原唱的伴奏发送音量设置为0 originOpt.playbackVolume = 0; //如果当前放纯音效音乐,将带原唱的播放发送音量设置为0 originOpt.sendWithAudioType = NERtcAudioStreamType.kNERtcAudioStreamTypeSub; //音效以辅流的方式发送 originOpt.startTimestamp = position; NERtcEx.getInstance().playEffect(originEffectId, originOpt); //自定义的带原唱的effectid -
主唱收到伴奏播放进度回调,并将自己的伴奏进度通过 SEI 发送出去。
Java@Override public void updateAudioEffectTimestamp(long effectId, long timestampMs) { //纯伴奏和带原唱的音效,两个进度是一样的,歌词进度只需要根据其中的一个音效进行同步 if(effectId != optOriginalEffectId) { return ; } if(主唱) { JSONObject jsonObject = new JSONObject(); try { jsonObject.put("audio_effect_pos", timestampMs); } catch (JSONException e) { e.printStackTrace(); } NERtcEx.getInstance().sendSEIMsg(jsonObject.toString()); } dataSource.pickService.musicPostion = timestampMs; //同步本地歌词进度 } -
合唱者混音。
串行合唱混音的详细示例代码请参见 串行合唱的混音代码(Android)。
Java//只有串行合唱模式下的合唱者,才需要去混音 @Override public void onPlaybackAudioFrameBeforeMixingWithUserID(long uid, NERtcAudioFrame neRtcAudioFrame) { //当前的演唱模式不是串行模式,则返回 if(dataSource.chorusMode != NERTC_CHORUS_SERIAL_MODE) { return ; } //自己不是合唱者,则返回 if(!isChrous) { return ; } //收到的uid不是主唱,则返回。只需要保持主唱的audioFrame if(uid != dataSource.anchorUserId) { return ; } try { NERtcAudioFormat format = neRtcAudioFrame.getFormat(); int length = format.getBytesPerSample() * format.getSamplesPerChannel() * format.getChannels(); byte[] buf = new byte[length]; neRtcAudioFrame.getData().get(buf); //===== 锁保护 ===== if(cacheFrames.size >= 10) { cacheFrames.poll(); } cacheFrames.offer(buf); //===== 锁保护 ===== } catch (Exception e) { e.printStackTrace(); } } //只有串行合唱模式下的合唱者,才需要去混音 @Override public void onRecordFrame(NERtcAudioFrame neRtcAudioFrame) { //当前的演唱模式不是串行模式,则返回 if(dataSource.chorusMode != NERTC_CHORUS_SERIAL_MODE) { return ; } //自己不是合唱者,则返回 if(!isChrous) { return ; } try { // ===== 锁保护 ====== if(cacheFrames.size() > 0) { byte[] buf = cacheFrames.poll(); if(buf == null) return; NERtcAudioFormat format = neRtcAudioFrame.getFormat(); int length = format.getBytesPerSample() * format.getSamplesPerChannel() * format.getChannels(); byte[] destBuffer = new byte[length]; ByteBuffer recordByteBuffer = neRtcAudioFrame.getData(); recordByteBuffer.position(0); recordByteBuffer.get(destBuffer,0,length); // mixer.mixAudioFrameData 的上下文请参见串行合唱的混音代码 byte[] buffer = mixer.mixAudioFrameData(destBuffer, buf, format.getSamplesPerChannel(), format.getChannels()); if(buffer == null) { ALog.i(LOG_TAG, "mixAudioFrameData buffer is null"); return ; } recordByteBuffer.position(0); recordByteBuffer.put(buffer); } // ===== 锁保护 ====== } catch (Exception e){ e.printStackTrace(); } } -
合唱者和观众同步歌词。
- 合唱者收到主唱的 SEI,同步自己的歌词进度,再通过 SEI 发送自己的歌词进度。
- 观众收到合唱者的 SEI,同步自己的歌词进度。
Java@Override public void onRecvSEIMsg(long l, String s) { long musicPosition = -1; try { JSONObject data = new JSONObject(s); musicPosition = Integer.parseInt(data.getString("audio_effect_pos")); } catch (Exception e) { e.printStackTrace(); } if(musicPosition == -1) { Log.i(TAG, "Error decode SEI message"); return ; } //合唱者收到主唱的SEI同步自己的歌词,并发送自己的歌词进度 if(isChrous) { JSONObject jsonObject = new JSONObject(); try { jsonObject.put("audio_effect_pos", musicPosition); } catch (JSONException e) { e.printStackTrace(); } NERtcEx.getInstance().sendSEIMsg(jsonObject.toString()); dataSource.pickService.musicPosition = musicPosition; } // 上麦者或者观众收到合唱的SEI,同步自己的歌词进度 else if(isSeat || isAudience) { dataSource.pickService.musicPosition = musicPosition; } } -
主唱结束合唱。
Java//主唱停止合唱 NERtcEx.getInstance().stopEffect(effectId) //恢复订阅副唱音频 NERtcEx.getInstance().subscribeRemoteAudioStream(chrousUid, true); //黑名单置空,恢复听众听到主唱声音 NERtcEx.getInstance().setAudioSubscribeOnlyBy(null); -
合唱者结束合唱。
Java//合唱者结束合唱 NERtcEx.getInstance().stopEffect(effectId) -
主唱和合唱者调用
setChannelProfile接口将房间场景设置为STANDARD_CHATROOM。Java
NERtcEx.getInstance().setChannelProfile(NERtcConstants.RTCChannelProfile.STANDARD_CHATROOM);
进阶功能
混响效果
开启混响音效功能时,SDK 耳返默认无混响效果,如果需要在耳返中体验混响效果,请用以下方式打开:
JavaNERtcParameters channelInfoResponse = new NERtcParameters();
NERtcParameters.Key keyChannelResponse =
NERtcParameters.Key.createSpecializedKey("engine.audio.earback.mode");
channelInfoResponse.set(keyChannelResponse, 0);
NERtcEx.getInstance().setParameters(channelInfoResponse);
主唱、合唱者静音和取消静音
建议通过以下方式静音和取消静音,以免损耗性能。该设置只影响麦克风采集音量,不影响发送的伴奏音量。
Java//mute
NERtcEx.getInstance().adjustRecordingSignalVolume(0);
//unmute
NERtcEx.getInstance().adjustRecordingSignalVolume(100);
切换原唱和伴奏
切换为播放原唱的示例代码如下:
Java//将带原唱的音效音量调整到正常,并将纯伴奏的音量调整成 0
NERtcEx.getInstance().setEffectSendVolume(pureEffectId, 0);
NERtcEx.getInstance().setEffectPlaybackVolume(pureEffectId, 0);
NERtcEx.getInstance().setEffectSendVolume(originEffectId, audioMixVolume);
NERtcEx.getInstance().setEffectPlaybackVolume(originEffectId, audioMixVolume);
切换为播放伴奏的示例代码如下:
Java//将带原唱的音效音量调整成 0,并将纯伴奏的音量调整成正常
[NERtcEngine.sharedEngine setEffectSendVolumeWithId:kEffectIdPureAccompantment volume:audioMixingVolume];
[NERtcEngine.sharedEngine setEffectPlaybackVolumeWithId:kEffectIdPureAccompantment volume:audioMixingVolume];
[NERtcEngine.sharedEngine setEffectSendVolumeWithId:kEffectIdOriginalSong volume:0];
[NERtcEngine.sharedEngine setEffectPlaybackVolumeWithId volume:0];
此文档是否对你有帮助?





