实现串行合唱

更新时间: 2023/05/12 08:18:23

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

功能原理

串行合唱的原理图如下。

串行合唱.png

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

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

功能介绍

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

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

前提条件

已实现加入和离开 RTC 房间。

合唱准备

在合唱准备阶段,您需要完成以下配置:

音频配置

  1. 在加入房间前调用 setChannelProfile 接口,设置房间场景为直播场景(LiveBroadcasting)。

  2. 在加入房间前调用 setAudioProfile 接口,设置音频 profile 类型为 HighQualityStereo,设置 scenarioMUSIC

  3. 设置 MixedAudioFrameRecordingAudioFrame 的混流参数。

    • 调用 channels 接口,设置音频推流声道数量为 2 。
    • 调用 sampleRate 接口,设置设备采样率为 48000。

示例代码如下:

NERtcEx.getInstance().setChannelProfile(NERtcConstants.RTCChannelProfile.LIVE_BROADCASTING);
NERtcEx.getInstance().setAudioProfile(NERtcConstants.AudioProfile.HIGH_QUALITY_STEREO, NERtcConstants.AudioScenario.MUSIC);

NERtcAudioFrameRequestFormat mixFormat = new NERtcAudioFrameRequestFormat();
mixFormat.setChannels(2);
mixFormat.setSampleRate(48000);
mixFormat.setOpMode(NERtcAudioFrameOpMode.kNERtcAudioFrameOpModeReadOnly);
NERtcEx.getInstance().setMixedAudioFrameParameters(mixFormat);
        
NERtcAudioFrameRequestFormat recordFormat = new NERtcAudioFrameRequestFormat();
recordFormat.setChannels(2);
recordFormat.setSampleRate(48000);
recordFormat.setOpMode(NERtcAudioFrameOpMode.kNERtcAudioFrameOpModeReadWrite);
NERtcEx.getInstance().setRecordingAudioFrameParameters(recordFormat);

音效优化

主唱和合唱者上麦

主唱和合唱者上麦

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

点歌

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

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

合唱

时序图

Android串行合唱时序图.png

实现流程

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

  2. 观众同意合唱,并申请上麦。上麦成功后,调用 enableLocalAudio 接口,将参数设置为 true,开启本地音频采集和发送。

    以下示例代码展示通过 IM 实现上麦,您可以自行实现相关逻辑。

    //申请上麦
    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);
    
    
  3. 主唱和合唱者在开始合唱前,调用如下代码开启 AEC 伴奏模式。

    开启 AEC 伴奏模式时,本端的人声保留比较好,有助于演唱者的唱歌体验。

    NERtcParameters mRtcParameters = new NERtcParameters();
    NERtcParameters.Key audioMixKey = NERtcParameters.Key.createSpecializedKey("key_audio_external_audio_mix");
    mRtcParameters.set(audioMixKey, true);
    NERtcEx.getInstance().setParameters(mRtcParameters);
    
  4. 唱歌开始前,设置主唱不订阅合唱者的声音,并且自己的声音只发给合唱者。

    NERtcEx.getInstance().subscribeRemoteAudioStream(chrousUid, false); //不订阅合唱者的音频
    long[] uids = new long[] {chrousUid};
    NERtcEx.getInstance().setAudioSubscribeOnlyBy(uids); //自己的音频只给合唱者
    
  5. 主唱调用 playEffect接口,以主流方式发送伴奏给合唱者。

    收到歌曲开始消息后,开始 3 秒倒计时,3 秒后开始播放伴奏。

    long 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
    
  6. 主唱收到伴奏播放进度回调,并将自己的伴奏进度通过 SEI 发送出去。

    @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; //同步本地歌词进度
    }
    
  7. 合唱者混音。

    串行合唱混音的详细示例代码请参见串行合唱的混音代码(Android)

    
    //只有串行合唱模式下的合唱者,才需要去混音
    @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();
        }
    }
    
    
  8. 合唱者和观众同步歌词。

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

//恢复订阅副唱音频
NERtcEx.getInstance().subscribeRemoteAudioStream(chrousUid, true);

//黑名单置空,恢复听众听到主唱声音
NERtcEx.getInstance().setAudioSubscribeOnlyBy(null);

//合唱者关闭低延迟模式
NERtcParameters neRtcParameters = new NERtcParameters();
NERtcParameters.Key privateJsonKey = NERtcParameters.Key.createSpecializedKey("engine.audio.ktv.chrous");
neRtcParameters.set(privateJsonKey, false);

//关闭AEC的伴奏模式
NERtcParameters.Key audioMixKey = NERtcParameters.Key.createSpecializedKey("key_audio_external_audio_mix");
neRtcParameters.set(audioMixKey, false);

NERtcEx.getInstance().setParameters(neRtcParameters); 
  1. 合唱者结束合唱。
//合唱者关闭低延时模式
NERtcParameters neRtcParameters = new NERtcParameters();
NERtcParameters.Key privateJsonKey = NERtcParameters.Key.createSpecializedKey("engine.audio.ktv.chrous");
neRtcParameters.set(privateJsonKey, false);

//关闭AEC的伴奏模式
NERtcParameters.Key audioMixKey = NERtcParameters.Key.createSpecializedKey("key_audio_external_audio_mix");
neRtcParameters.set(audioMixKey, false);

NERtcEx.getInstance().setParameters(neRtcParameters); 

进阶功能

主唱、合唱者静音和取消静音

建议通过以下方式静音和取消静音,以免损耗性能。该设置只影响麦克风采集音量,不影响发送的伴奏音量。

//mute
NERtcEx.getInstance().adjustRecordingSignalVolume(0);
//unmute
NERtcEx.getInstance().adjustRecordingSignalVolume(100);

切换原唱和伴奏

切换为播放原唱的示例代码如下:

//将带原唱的音效音量调整到正常,并将纯伴奏的音量调整成 0
NERtcEx.getInstance().setEffectSendVolume(pureEffectId, 0);
NERtcEx.getInstance().setEffectPlaybackVolume(pureEffectId, 0);
NERtcEx.getInstance().setEffectSendVolume(originEffectId, audioMixVolume);
NERtcEx.getInstance().setEffectPlaybackVolume(originEffectId, audioMixVolume);

切换为播放伴奏的示例代码如下:

//将带原唱的音效音量调整成 0,并将纯伴奏的音量调整成正常
[NERtcEngine.sharedEngine setEffectSendVolumeWithId:kEffectIdPureAccompantment volume:audioMixingVolume];
[NERtcEngine.sharedEngine setEffectPlaybackVolumeWithId:kEffectIdPureAccompantment volume:audioMixingVolume];
[NERtcEngine.sharedEngine setEffectSendVolumeWithId:kEffectIdOriginalSong volume:0];
[NERtcEngine.sharedEngine setEffectPlaybackVolumeWithId volume:0];
此文档是否对你有帮助?
有帮助
去反馈
  • 功能原理
  • 功能介绍
  • 前提条件
  • 合唱准备
  • 音频配置
  • 音效优化
  • 主唱和合唱者上麦
  • 主唱和合唱者上麦
  • 点歌
  • 合唱
  • 进阶功能
  • 主唱、合唱者静音和取消静音
  • 切换原唱和伴奏