实现串行合唱

更新时间: 2025/09/22 13:44:12

串行合唱场景中,主唱将伴奏和人声发送给合唱者,合唱者根据主唱的伴奏进行合唱,并混流发给观众。

功能原理

串行合唱的原理图如下。

串行合唱.png

串行合唱的原理说明如下:

  1. 主唱播放伴奏,并以主流方式发送伴奏。
  2. 主唱的 NERTC SDK 将主唱的干声(Audio1)和伴奏合流后发给合唱者,主唱的干声(Audio1)和伴奏只有合唱者才能听到,主唱不订阅合唱者的干声。
  3. 合唱者将伴奏、主唱的干声(Audio1)和合唱者自己的干声(Audio2)混音后发给观众。
  4. 观众只听合唱者的声音。

功能介绍

NTP 实时合唱和串行合唱的方案对比如下表所示。

维度 NTP 实时合唱 串行合唱
主唱体验 能实时听到伴奏和合唱者的声音 只能听到伴奏,不能听到合唱者的声音
合唱者体验 能实时听到伴奏和主唱的声音 听到伴奏和主唱同步
观众体验 能实时听到伴奏、主唱的声音、合唱者的声音,且三者同步 听到伴奏、主唱的声音、合唱者的声音,且三者同步
硬件要求 对网络和机型有一定要求 抗弱网性能好,机型覆盖全
实现难度 接入成本低 接入成本中等

前提条件

主唱和合唱者上麦

麦位相关的实现逻辑需要业务自行实现。

点歌

  1. 麦位上的用户可以点歌,具体实现逻辑需要业务自行实现。

  2. 下载歌曲文件和并展现歌词,具体实现请参见 版权音乐歌词展示与同步

合唱

时序图

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

实现流程

  1. 主唱发起合唱邀请,具体实现方式需要业务自行实现。

  2. 观众同意合唱,并申请上麦。上麦成功后,调用 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);
    
  1. 合唱前,主唱和合唱者调用 setChannelProfile 接口将房间场景设置为 Karaoke 场景。

    JavaNERtcEx.getInstance().setChannelProfile(NERtcConstants.RTCChannelProfile.Karaoke);
    
  2. 唱歌开始前,设置主唱不订阅合唱者的声音,并且自己的声音只发给合唱者。

    JavaNERtcEx.getInstance().subscribeRemoteAudioStream(chrousUid, false); //不订阅合唱者的音频
    long[] uids = new long[] {chrousUid};
    NERtcEx.getInstance().setAudioSubscribeOnlyBy(uids); //自己的音频只给合唱者
    
  3. 主唱调用 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
    
  4. 主唱收到伴奏播放进度回调,并将自己的伴奏进度通过 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; //同步本地歌词进度
    }
    
  5. 合唱者混音。

    串行合唱混音的详细示例代码请参见 串行合唱的混音代码(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();
        }
    }
    
  6. 合唱者和观众同步歌词。

    1. 合唱者收到主唱的 SEI,同步自己的歌词进度,再通过 SEI 发送自己的歌词进度。
    2. 观众收到合唱者的 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;
        }
    }
    
  7. 主唱结束合唱。

    Java//主唱停止合唱
    NERtcEx.getInstance().stopEffect(effectId)
    
    //恢复订阅副唱音频
    NERtcEx.getInstance().subscribeRemoteAudioStream(chrousUid, true);
    
    //黑名单置空,恢复听众听到主唱声音
    NERtcEx.getInstance().setAudioSubscribeOnlyBy(null);
    
  8. 合唱者结束合唱。

    Java//合唱者结束合唱
    NERtcEx.getInstance().stopEffect(effectId)
    
  9. 主唱和合唱者调用 setChannelProfile 接口将房间场景设置为 STANDARD_CHATROOM

    JavaNERtcEx.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];
此文档是否对你有帮助?
有帮助
去反馈
  • 功能原理
  • 功能介绍
  • 前提条件
  • 主唱和合唱者上麦
  • 点歌
  • 合唱
  • 进阶功能
  • 混响效果
  • 主唱、合唱者静音和取消静音
  • 切换原唱和伴奏