实现NTP实时合唱

更新时间: 2022/11/11 10:00:12

NTP 实时合唱方案通过 NTP 同步多台设备,使得双方在在弱网情况下也能精准同步,保证演唱者体验。实时合唱技术避免了合唱者对主唱伴奏的依赖,双方同时起步声音延迟更低。

注意事项

  • 主唱和合唱者都需要播放伴奏。
  • 主唱以辅流方式发送伴奏,合唱者不发送伴奏。
  • 合唱者不订阅主唱的辅流音频。
  • 主唱和合唱者在合唱开始时,需要打开低延时模式。

功能原理

NTP 实时合唱的原理图如下。

NTP实时合唱.png

NTP 实时合唱的原理说明如下:

  1. 主唱本地播放伴奏,以辅流方式发送伴奏给 RTC 服务器。
  2. 合唱者本地播放伴奏,不订阅主唱的伴奏(辅流),也不发送伴奏给 RTC 服务器。
  3. 主唱和合唱者将自己干声发送给 RTC 服务器。RTC 服务器将伴奏、主唱干声(Audio1)、合唱者干声(Audio2)通过SDK 精准同步混流后,发给观众。
  4. 主唱能同时听到合唱者的干声(Audio2)。

NTP对齐的原理说明如下:

在实时合唱方案中,需要在开唱后实时同步伴奏进度,避免因伴奏误差而增加端到端延迟。但不同设备的本地时钟并不一致,存在一定误差,因此需要主唱和合唱者通过 NTP 对齐播放伴奏的时间,达到实时的效果,对齐伴奏的原理说明如下:

  1. 主唱和合唱者分别获取本地系统时间与 RTC 服务端时间的 NTP 时间差。
  2. 主唱推送本地播放伴奏的开始时间和倒计时时间,并根据主唱的 NTP 时间差,换算成 RTC 服务端的 UTC 时间。
  3. 合唱者根据 RTC 服务端的 UTC 时间以及合唱者的 NTP 时间差,计算得出本地播放伴奏的 UTC 时间。

功能介绍

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

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

前提条件

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

合唱准备

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

设置 NTP 对齐

在实时合唱方案中,需要在开唱后实时同步伴奏进度,避免因伴奏误差而增加端到端延迟。但不同设备的本地时钟并不一致,存在一定误差,因此需要主唱和合唱者通过 NTP 对齐播放伴奏的时间,达到实时的效果。

  1. 进入房间前,所有用户调用 setStreamAlignmentProperty 接口,将参数值设置为 YES,设置 NTP 时间对齐。

    设置成功后,设备会多次与 RTC 服务器校准 NTP 值,该配置能让 getNtpTimeOffset 的值更加精准。

    示例代码如下:

    // 加入房间前设置,设置了NTP 精准对齐
    [[NERtcEngine sharedEngine] setStreamAlignmentProperty:YES];
    
  2. 开始合唱前,主唱和合唱者分别获取本地系统时间与 RTC 服务端时间的 NTP 时间差。

    - (int64_t)getNtpTimeOffset;
    
  3. 主唱或合唱者收到开始合唱信令, 主唱本地播放伴奏,并发送播放伴奏信令给合唱者。

    例如,主唱播放伴奏的时间设置为 3 秒之后,需要等合唱者同一时间一起播放伴奏。

    1. NERtcCreateAudioEffectOptionstartTimeStamp 设置为 UTC时间 + 倒计时时间。

      SInt64 startTimeStamp = ([NSDate date].timeIntervalSince1970)* 1000 + 3*1000;
      NERtcCreateAudioEffectOption *opt = [[NERtcCreateAudioEffectOption alloc] init];
      opt.startTimeStamp = startTimeStamp;
      
    2. 主唱发送播放伴奏信令给合唱者。信令中包含当前服务器的 UTC 时间 + 倒计时时间。

      - (void)sendChorusSignal {
          SInt64 localTimeStamp = ([NSDate date].timeIntervalSince1970)* 1000;
          SInt64 ntpOffsetTime = engine.getNtpTimeOffset;
          SInt64 serverTimeStamp = localTimeStamp - ntpOffsetTime; //当前服务器的UTC时间
          SInt64 serverPlayTimeStamp = serverTimeStamp+3*1000; //正式开始播放伴奏的服务器 UTC时间
          //发送播放伴奏信令,信令中包含serverPlayTimeStamp的值
          [[NIMSDK sharedSDK].chatManager sendMessage:message];
      }
      
  4. 合唱者收到主唱发送的播放伴奏信令,根据播放伴奏的服务器 UTC 时间 + 自己的 NTP 时间差,计算得到自己播放伴奏的 UTC 时间。

    - (void)didReceiveChorusSignaMessage:(NIMMessage *)customMessage {
        SInt64 serverPlayTimeStamp = customMessage.chorusAttchment.serverPlayTimeStamp;//服务器正式播放伴音的UTC时间
        SInt64 ntpOffsetTime = engine.getNtpTimeOffset;
        SInt64 localPlayTimeStamp = serverPlayTimeStamp + ntpOffsetTime; //当前设备播放伴音的UTC时间
        NERtcCreateAudioEffectOption *opt = [[NERtcCreateAudioEffectOption alloc] init];
        opt.startTimeStamp = localPlayTimeStamp;
    }
    

主唱开启低延时模式

[[NERtcEngine sharedEngine] setParameters:@{@"engine.audio.ktv.chrous": @YES}];

音频配置

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

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

  3. 设置 MixedAudioFrameRecordingAudioFrame 的混流参数。

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

示例代码如下:

[[NERtcEngine sharedEngine] setChannelProfile:kNERtcChannelProfileLiveBroadcasting];
[[NERtcEngine sharedEngine] setAudioProfile:kNERtcAudioProfileHighQualityStereo scenario:kNERtcAudioScenarioMusic];

NERtcAudioFrameRequestFormat *format = [[NERtcAudioFrameRequestFormat alloc] init];
format.channels = 2;
format.sampleRate = 48000;
format.mode = kNERtcAudioFrameOpModeReadOnly;
[[NERtcEngine sharedEngine] setMixedAudioFrameParameters:format];

NERtcAudioFrameRequestFormat *aFormat = [[NERtcAudioFrameRequestFormat alloc] init];
aFormat.channels = 2;
aFormat.sampleRate = 48000;
aFormat.mode = kNERtcAudioFrameOpModeReadWrite;
[[NERtcEngine sharedEngine] setRecordingAudioFrameParameters:aFormat];

音效优化

主唱和合唱者上麦

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

点歌

  1. 麦位上的用户可以点歌,具体实现逻辑实现需要业务自行实现。
  2. 下载歌曲文件和并展现歌词,具体实现请参见版权音乐歌词展示与同步

合唱

时序图

实时合唱.jpg

实现流程

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

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

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

    NSString *strUserID = [NSString stringWithFormat:@"%@",@(self.context.myAccountInfo.uid)];
    NSDictionary *dict = @{@"type":@(NTESVoiceChatAttachmentTypeJoinChorusRequest),
                                 @"data":@{
                                         @"userId":strUserID,
                                         @"nickName":self.context.myAccountInfo.nickName
                                        }
                                 };
    NIMMessage *message = [NTESChorusMessageHelper messageWithChorusDict:dict];
    [[NIMSDK sharedSDK].chatManager sendMessage:message toSession:self.session error:nil];
    
    
  3. 主唱和合唱者在开始合唱前,调用如下代码开启 AEC 伴奏模式。

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

    [NERtcEngine.sharedEngine setParameters:@{@"kNERtcKeyAudioProcessingExternalAudioMixEnable": @YES}];
    
    
  4. 合唱者在本地播放伴奏,等合唱者完成下载歌曲文件和歌词后,开始合唱。

  5. 设置合唱者不订阅主唱的伴奏。

    [NERtcEngine.sharedEngine subscribeRemoteSubStreamAudio:NO forUserID:主唱UserID];
    
  6. 主唱以辅流方式发送伴奏给合唱者和观众。

    SInt64 position = ([NSDate date].timeIntervalSince1970)* 1000 + kCountTimeLastSec *1000;
    NERtcCreateAudioEffectOption *optPure = [[NERtcCreateAudioEffectOption alloc] init];
    optPure.path = 纯伴奏音乐路径
    optPure.loopCount = 1;
    optPure.sendEnabled = YES;
    optPure.sendVolume = 发送音量; 
    optPure.playbackVolume = 播放音量;
    optPure.startTimeStamp = position;
    [NERtcEngine.sharedEngine playEffectWitdId:kEffectIdPureAccompantment effectOption:optPure]; // kEffectIdPureAccompantment  自己定义effect id
    
    NERtcCreateAudioEffectOption *optOriginalSong = [[NERtcCreateAudioEffectOption alloc] init];
    optOriginalSong.path = 原唱伴奏音乐路径
    optOriginalSong.loopCount = 1;
    optOriginalSong.sendEnabled = YES;
    optOriginalSong.sendVolume = 0; //如果当前放纯伴奏音乐,将带原唱的伴奏发送音量设成0
    optOriginalSong.playbackVolume = 0; //如果当前放纯伴奏音乐,将带原唱的伴奏发送音量设成0
    optOriginalSong.startTimeStamp = position;
    
    if (self.context.chorusMode == NTESChorusModeRealtime) {
        optPure.sendWithAudioType = kNERtcAudioStreamTypeSub; //伴奏以辅流方式发送
        optOriginalSong.sendWithAudioType = kNERtcAudioStreamTypeSub; //伴奏以辅流方式发送
        [NERtcEngine.sharedEngine enableLocalSubStreamAudio:YES]; //开启音频辅流
    }
    
    [NERtcEngine.sharedEngine playEffectWitdId:kEffectIdOriginalSong effectOption:optOriginalSong]; // kEffectIdOriginalSong  自己定义effect id
    
  7. 主唱和合唱者通过onAudioEffectTimestampUpdateWithId同步本地歌词进度。 主唱调用 sendSEIMsg 接口,发送播放进度给观众。

    - (void)onAudioEffectTimestampUpdateWithId:(uint32_t)effectId timeStampMS:(uint64_t)timeStampMS  {
        //纯伴奏和带原唱的伴奏,两个进度是一样的,歌词进度只需根据其中一个伴奏的进度进行同步
        if(effectId != kEffectIdPureAccompantment) 
            return;  
            
        if (主唱) {
            NSMutableDictionary *dict = [NSMutableDictionary dictionary];
            dict[kAudioMixingPos] = @(timeStampMS);
            NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
            [NERtcEngine.sharedEngine sendSEIMsg:data];
        }
        
        //主唱和合唱者同步本地歌词进度
        self.dataSource.pickService.musicPosition = timeStampMS;
    }
    
  8. 观众根据收到的 onNERtcEngineRecvSEIMsg 回调,同步歌词进度。

    - (void)onNERtcEngineRecvSEIMsg:(uint64_t)userID message:(NSData *)message {
        
        NSError *error;
        NSDictionary *JSONObject = [NSJSONSerialization JSONObjectWithData:message options:0 error:&error];
        if (error) {
            return NELPLogError(@"Error decode SEI message: %@", error);
        }
        
        if (听众) {
            self.musicPosition = (uint32_t)[JSONObject[kAudioMixingPos] integerValue];
            self.dataSource.pickService.musicPosition = self.musicPosition;
        }
    }
    
  9. 合唱者开始本地播放伴奏,但不发送伴奏。

    SInt64 ntpTimeOffset = [NERtcEngine sharedEngine].getNtpTimeOffset;
    SInt64 timeStamp = chorusAttchment.chorusModel.ntp_time_stamp + context.rtcConfig.timeOffset;
    SInt64 localTimeStamp = timeStamp + ntpTimeOffset + chorusAttchment.chorusModel.audio_offset;
    
    NERtcEngine *engine = [NERtcEngine sharedEngine];
    NERtcCreateAudioEffectOption *optPure = [[NERtcCreateAudioEffectOption alloc] init];
    optPure.path = 纯伴奏音乐路径
    optPure.loopCount = 1;
    optPure.sendEnabled = NO;
    optPure.sendVolume = 发送音量; 
    optPure.playbackVolume = 播放音量;
    optPure.startTimeStamp = localTimeStamp;
    [NERtcEngine.sharedEngine playEffectWitdId:kEffectIdPureAccompantment effectOption:optPure]; // kEffectIdPureAccompantment  自己定义effect id
    
    NERtcCreateAudioEffectOption *optOriginalSong = [[NERtcCreateAudioEffectOption alloc] init];
    optOriginalSong.path = 原唱伴奏音乐路径
    optOriginalSong.loopCount = 1;
    optOriginalSong.sendEnabled = NO;
    optOriginalSong.sendVolume = 0; //如果当前放纯伴奏音乐,将带原唱的伴奏发送音量设成0
    optOriginalSong.playbackVolume = 0; //如果当前放纯伴奏音乐,将带原唱的伴奏发送音量设成0
    optOriginalSong.startTimeStamp = localTimeStamp;
    [NERtcEngine.sharedEngine playEffectWitdId:kEffectIdOriginalSong effectOption:optOriginalSong]; // kEffectIdOriginalSong  自己定义effect id;
    
  10. 主唱结束合唱。

    //主唱关闭低延时模式
    [[NERtcEngine sharedEngine] setParameters:@{@"engine.audio.ktv.chrous": @NO}];
    
    //主唱关闭辅流
    [NERtcEngine.sharedEngine enableLocalSubStreamAudio:NO];
        
    //关闭AEC的伴奏模式
    [NERtcEngine.sharedEngine setParameters:@{@"kNERtcKeyAudioProcessingExternalAudioMixEnable": @NO}];
    
    
  11. 合唱者结束合唱。

    //合唱者关闭低延时模式
    [[NERtcEngine sharedEngine] setParameters:@{@"engine.audio.ktv.chrous": @NO}];
    
    //合唱者恢复订阅主唱伴奏
    [NERtcEngine.sharedEngine subscribeRemoteSubStreamAudio:YES forUserID:主唱UserID];
    
    //关闭AEC的伴奏模式
    [NERtcEngine.sharedEngine setParameters:@{@"kNERtcKeyAudioProcessingExternalAudioMixEnable": @NO}];
    
    

进阶功能

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

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

//mute
[[NERtcEngine sharedEngine] adjustRecordingSignalVolume:0];
//unmute
[[NERtcEngine sharedEngine] adjustRecordingSignalVolume:100];

切换原唱和伴奏

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

//将带原唱的音量调整到正常,并将纯伴奏的音量调整成 0
[NERtcEngine.sharedEngine setEffectSendVolumeWithId:kEffectIdPureAccompantment volume:0];
[NERtcEngine.sharedEngine setEffectPlaybackVolumeWithId:kEffectIdPureAccompantment volume:0];
[NERtcEngine.sharedEngine setEffectSendVolumeWithId:kEffectIdOriginalSong volume:audioMixingVolume];
[NERtcEngine.sharedEngine setEffectPlaybackVolumeWithId:kEffectIdOriginalSong volume:audioMixingVolume];

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

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