实现串行合唱
更新时间: 2024/11/26 15:44:05
串行合唱场景中,主唱将伴奏和人声发送给合唱者,合唱者根据主唱的伴奏进行合唱,并混流发给观众。
前提条件
功能原理
串行合唱的原理图如下。
串行合唱的原理说明如下:
- 主唱播放伴奏,并以主流方式发送伴奏。
- 主唱的 NERTC SDK 将主唱的干声(Audio1)和伴奏合流后发给合唱者,主唱的干声(Audio1)和伴奏只有合唱者才能听到,主唱不订阅合唱者的干声。
- 合唱者将伴奏、主唱的干声(Audio1)和合唱者自己的干声(Audio2)混音后发给观众。
- 观众只听合唱者的声音。
功能介绍
NTP 实时合唱和串行合唱的方案对比如下表所示。
维度 | NTP 实时合唱 | 串行合唱 |
---|---|---|
主唱体验 | 能实时听到伴奏和合唱者的声音 | 只能听到伴奏,不能听到合唱者的声音 |
合唱者体验 | 能实时听到伴奏和主唱的声音 | 听到伴奏和主唱同步 |
观众体验 | 能实时听到伴奏、主唱的声音、合唱者的声音,且三者同步 | 听到伴奏、主唱的声音、合唱者的声音,且三者同步 |
硬件要求 | 对网络和机型有一定要求 | 抗弱网性能好,机型覆盖全 |
实现难度 | 接入成本低 | 接入成本中等 |
合唱准备
在合唱准备阶段,您需要完成以下配置:
音频配置
-
在加入房间前调用
setChannelProfile
接口,设置房间场景为直播场景(LiveBroadcasting
)。 -
在加入房间前调用
setAudioProfile
接口,设置音频profile
类型为HighQualityStereo
,设置scenario
为MUSIC
。 -
设置
MixedAudioFrame
和RecordingAudioFrame
的混流参数。- 调用
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];
主唱和合唱者上麦
麦位相关的实现逻辑需要业务自行实现。
点歌
合唱
时序图
实现流程
-
主唱发起合唱邀请,具体实现方式需要业务自行实现。
-
观众同意合唱,并申请上麦。上麦成功后,调用
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];
-
主唱和合唱者在开始合唱前,调用如下代码开启 AEC 伴奏模式。
开启 AEC 伴奏模式时,本端的人声保留比较好,有助于演唱者的唱歌体验。
[NERtcEngine.sharedEngine setParameters:@{@"kNERtcKeyAudioProcessingExternalAudioMixEnable": @YES}];
-
唱歌开始前,设置主唱不订阅合唱者的声音,并且自己的声音只发给合唱者。
[NERtcEngine.sharedEngine subscribeRemoteAudio:NO forUserID:userID]; //不订阅合唱者的音频 [NERtcEngine.sharedEngine setAudioSubscribeOnlyBy:@[@(userID)]]; //自己的音频只给合唱者
-
主唱调用
playEffectWitdId
接口,以主流方式发送伴奏给合唱者。收到歌曲开始消息后,开始 3 秒倒计时,3 秒后开始播放伴奏。
SInt64 position = ([NSDate date].timeIntervalSince1970)* 1000 + 3 * 1000; //3 秒后开始播放伴奏 NERtcCreateAudioEffectOption *optPure = [[NERtcCreateAudioEffectOption alloc] init]; optPure.path = 纯伴奏音乐路径 optPure.loopCount = 1; optPure.sendEnabled = YES; optPure.sendVolume = 发送音量; optPure.playbackVolume = 播放音量; optPure.sendWithAudioType = kNERtcAudioStreamTypeMain; //伴奏以主流方式发送 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.sendWithAudioType = kNERtcAudioStreamTypeMain; //伴奏以主流方式发送 optOriginalSong.startTimeStamp = position; [NERtcEngine.sharedEngine playEffectWitdId:kEffectIdOriginalSong effectOption:optOriginalSong]; // kEffectIdOriginalSong 自己定义effect id
-
主唱收到伴奏播放进度回调,并将自己的伴奏进度通过 SEI 发送出去。
- (void)onAudioEffectTimestampUpdateWithId:(uint32_t)effectId timeStampMS:(uint64_t)timeStampMS { //纯伴奏和带原唱的伴奏,两个进度是一样的,歌词进度只需根据其中一个伴奏的进度进行同步 if(effectId != kEffectIdPureAccompantment) return; 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;//同步本地歌词进度 }
-
合唱者混音。
串行合唱混音的详细示例代码请参见串行合唱的环形Buffer和混音代码。
//只有串行合唱模式下的合唱者,才需要去混音 - (void)onNERtcEnginePlaybackAudioFrameBeforeMixingWithUserID:(uint64_t)userID frame:(NERtcAudioFrame *)frame { //当前的演唱模式不是串行模式,则返回 if (_dataSource.chorusMode != NTESChorusModeSerial) return; //自己不是合唱者,则返回 if(!_dataSource.selfIsSuperChorus) return; //收到的userID不是主唱,则返回。 只需要保持主唱的audioFrame if(userID != _dataSource.superAnchorUserID) return; int32_t size = frame.format.channels * frame.format.bytesPerSample * frame.format.samplesPerChannel; //环形buffer保存audioFrame,避免不断地alloc,free,造成内存碎片,具体上下文结构见上面的示例代码 NMCTPCircularBufferProduceBytes(_recvBuffer,frame.data,size); _audioChannels = frame.format.channels; _samplesPerChannel = frame.format.samplesPerChannel; //留一点buffer,保证AudioFrameDidRecord时,有数据,避免听众听到的声音不流畅。 if (_recvBuffer->fillCount >= 5*size) { _bStartMix = true; } } //只有串行合唱模式下的合唱者,才需要去混音 - (void)onNERtcEngineAudioFrameDidRecord:(NERtcAudioFrame *)frame { //当前的演唱模式不是串行模式,则返回 if (_dataSource.chorusMode != NTESChorusModeSerial) return; //自己不是合唱者,则返回 if(!_dataSource.selfIsSuperChorus) return; //保证环形buffer里有些数据缓存之后,再开始混音 if (!_bStartMix) return; //ChannelProfile之前已设为LiveBroadcasting。 //MixedAudioFrame 和 RecordingAudioFrame 需要设为 channels 为 2, sampleRate 为 48000 //如果以上2个条件没设,主唱和合唱者的audioFrame格式不一致,没法混音。 if (_audioChannels != frame.format.channels || _samplesPerChannel != frame.format.samplesPerChannel) return; int32_t availableBytes = 0; int32_t size = frame.format.channels * frame.format.bytesPerSample * frame.format.samplesPerChannel; void *buffer = NMCTPCircularBufferTail(_recvBuffer, &availableBytes); if (availableBytes >= size) { [PlayerAudioMixer mixAudioFrameData:frame.data data2:buffer samplesPerChannel:frame.format.samplesPerChannel channels:frame.format.channels]; NMCTPCircularBufferConsume(_recvBuffer, size); } }
-
合唱者和观众同步歌词。
- 合唱者收到主唱的 SEI,同步自己的歌词进度,再通过 SEI 发送自己的歌词进度。
- 观众收到合唱者的 SEI,同步自己的歌词进度。
- (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); } //合唱者收到主唱的 SEI 同步自己的歌词,并发送自己的歌词进度 if (_dataSource.selfIsSuperChorus) { uint32_t musicPosition = (uint32_t)[JSONObject[kAudioMixingPos] integerValue]; self.dataSource.pickService.musicPosition = musicPosition; NSDictionary *dict = @{kAudioMixingPos: @(musicPosition)}; NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; [NERtcEngine.sharedEngine sendSEIMsg:data]; } //观众或上麦者收到合唱者的 SEI, 同步自己的歌词进度 else if(userID == _dataSource.firstSuperChorusUserID){ uint32_t musicPosition = (uint32_t)[JSONObject[kAudioMixingPos] integerValue]; self.dataSource.pickService.musicPosition = musicPosition; } }
-
主唱结束合唱。
//恢复订阅副唱音频
[NERtcEngine.sharedEngine subscribeRemoteAudio:YES forUserID:userID];
//黑名单置空, 恢复听众听到主唱声音
[NERtcEngine.sharedEngine setAudioSubscribeOnlyBy:@[]];
//合唱者关闭低延时模式
[[NERtcEngine sharedEngine] setParameters:@{@"engine.audio.ktv.chrous": @NO}];
//关闭AEC的伴奏模式
[NERtcEngine.sharedEngine setParameters:@{@"kNERtcKeyAudioProcessingExternalAudioMixEnable": @NO}];
- 合唱者结束合唱。
//合唱者关闭低延时模式
[[NERtcEngine sharedEngine] setParameters:@{@"engine.audio.ktv.chrous": @NO}];
//关闭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];