空间音效
声网为电子竞技、在线会议等场景提供空间音效功能,让用户拥有沉浸式的音频体验。
- 通过更新用户的空间位置,用户可以实时感受到其他用户距离、方位、朝向的变化。
- 通过更新媒体播放器的空间位置,用户可以为背景音、伴奏等媒体资源增添空间感。
- 通过设置参数,体验声音模糊、空气衰减等效果。
空间音效功能当前处于实验阶段,请联系 sales@shengwang.cn 开通空间音效功能,如果需要技术支持,请联系技术支持。
技术原理
用户的空间音效
声网为用户提供本地直角坐标系计算方案设置空间音效:
使用 ILocalSpatialAudioEngine
类实现空间音效,通过 SDK 计算本地用户与远端用户的相对位置。你需要分别调用 updateSelfPosition
和 updateRemotePosition
更新本地和远端用户的空间坐标,本地用户才能听到远端用户的空间音效。
媒体播放器的空间音效
声网为媒体播放器提供本地直角坐标系计算方案设置空间音效:
通过 SDK 计算本地用户和媒体播放器的相对位置。你需要在 ILocalSpatialAudioEngine
类中分别调用 updateSelfPosition
和 updatePlayerPositionInfo
更新本地用户和媒体播放器的空间坐标,本地用户才能听到媒体播放器的空间音效。
前提条件
在进行操作之前,请确保你已经在项目中实现了基本的实时音视频功能。详见实现音视频互动。
实现方法
用户的空间音效
该方案通过 ILocalSpatialAudioEngine
类实现空间音效,API 调用时序与操作步骤如下:
-
在调用其他声网 API 前,先后调用
RtcEngine
类的create
并填入你的 App ID,初始化RtcEngine
对象。 -
在调用
ILocalSpatialAudioEngine
类的其他 API 前,调用ILocalSpatialAudioEngine
类的create
和initialize
初始化ILocalSpatialAudioEngine
对象。 -
调用
setAudioProfile
设置profile
(音频编码属性)为AUDIO_PROFILE_DEFAULT
、scenario
(音频应用场景)为AUDIO_SCENARIO_GAME_STREAMING
。 -
调用带
options
参数的joinChannel
加入频道(使用 RTC Token)。你需要在ChannelMediaOptions
中设置channelProfile
(频道场景)为CHANNEL_PROFILE_LIVE_BROADCASTING
(直播场景)、clientRoleType
(用户角色)为CLIENT_ROLE_BROADCASTER
(主播)。 -
声网默认订阅所有远端用户的音频流,你需要调用
RtcEngine
的muteAllRemoteAudioStreams(true)
取消订阅所有远端用户,否则你在步骤 6 中设置的音频接收范围会无效。 -
调用下列方法,设置音频接收范围:
- 调用
setMaxAudioRecvCount
设置音频接收范围内最多可接收的音频流数。 - 调用
setAudioRecvRange
设置音频接收范围(米)。 - 调用
setDistanceUnit
设置游戏引擎单位距离的长度(米)。
- 调用
-
先后调用
updateSelfPosition
和updateRemotePosition
更新本地和远端用户的空间位置,体验空间音效。 -
如果无需体验空间音效,则调用
clearRemotePositions
删除所有远端用户的空间位置信息。删除后,本地用户会听不到所有远端用户。 -
调用
ILocalSpatialAudioEngine
的destroy
销毁ILocalSpatialAudioEngine
对象。注意必须在调用
RtcEngine
的destroy
前销毁ILocalSpatialAudioEngine
对象。 -
调用
RtcEngine
的leaveChannel
和destroy
离开频道并销毁RtcEngine
对象。
媒体播放器的空间音效
该方案通过 ILocalSpatialAudioEngine
类中的 updateSelfPosition
和 updatePlayerPositionInfo
实现空间音效。以 ILocalSpatialAudioEngine
类为例,操作步骤如下:
-
在调用其他声网 API 前,先后调用
RtcEngine
类的create
并填入你的 App ID,初始化RtcEngine
对象。 -
在调用
ILocalSpatialAudioEngine
类的其他 API 前,调用ILocalSpatialAudioEngine
类的create
和initialize
初始化ILocalSpatialAudioEngine
对象。 -
调用
setAudioProfile
设置profile
(音频编码属性)为AUDIO_PROFILE_DEFAULT
、scenario
(音频应用场景)为AUDIO_SCENARIO_GAME_STREAMING
。 -
调用带
options
参数的joinChannel
加入频道(使用 RTC Token)。你需要在ChannelMediaOptions
中设置channelProfile
(频道场景)为CHANNEL_PROFILE_LIVE_BROADCASTING
(直播场景)、clientRoleType
(用户角色)为CLIENT_ROLE_BROADCASTER
(主播)。 -
声网默认订阅所有远端用户的音频流,你需要调用
RtcEngine
的muteAllRemoteAudioStreams(true)
取消订阅所有远端用户,否则你在步骤 6 中设置的音频接收范围会无效。 -
调用下列方法,设置音频接收范围:
- 调用
setMaxAudioRecvCount
设置音频接收范围内最多可接收的音频流数。 - 调用
setAudioRecvRange
设置音频接收范围(米)。 - 调用
setDistanceUnit
设置游戏引擎单位距离的长度(米)。
- 调用
-
先后调用
updateSelfPosition
和updatePlayerPositionInfo
更新本地用户和媒体播放器的空间位置,体验空间音效。 -
调用
ILocalSpatialAudioEngine
的destroy
销毁ILocalSpatialAudioEngine
对象。注意必须在调用
RtcEngine
的destroy
前销毁ILocalSpatialAudioEngine
对象。 -
调用
RtcEngine
的destroy
销毁RtcEngine
对象。
示例代码
本节展示使用本地直角坐标系计算方案实现媒体播放器空间音效的示例代码:
import static io.agora.api.example.common.model.Examples.ADVANCED;
import static io.agora.mediaplayer.Constants.MediaPlayerState.PLAYER_STATE_IDLE;
import static io.agora.mediaplayer.Constants.MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED;
import static io.agora.mediaplayer.Constants.MediaPlayerState.PLAYER_STATE_PLAYBACK_COMPLETED;
import static io.agora.mediaplayer.Constants.MediaPlayerState.PLAYER_STATE_STOPPED;
import io.agora.api.example.common.Constant;
import io.agora.mediaplayer.Constants;
import io.agora.mediaplayer.IMediaPlayer;
import io.agora.mediaplayer.IMediaPlayerObserver;
import io.agora.mediaplayer.data.PlayerUpdatedInfo;
import io.agora.mediaplayer.data.SrcInfo;
import io.agora.rtc2.IRtcEngineEventHandler;
import io.agora.rtc2.RtcEngine;
import io.agora.rtc2.SpatialAudioParams;
import io.agora.spatialaudio.ILocalSpatialAudioEngine;
import io.agora.spatialaudio.LocalSpatialAudioConfig;
import io.agora.spatialaudio.RemoteVoicePositionInfo;
public class SpatialSound extends BaseFragment {
private RtcEngine engine;
private IMediaPlayer mediaPlayer;
private ILocalSpatialAudioEngine localSpatial;
private RemoteVoicePositionInfo positionInfo = new RemoteVoicePositionInfo();
private int speakerUid;
@Nullable
@Override
private void startRecord() {
startTv.setVisibility(View.GONE);
// 打开媒体资源
mediaPlayer.open(Constant.URL_PLAY_AUDIO_FILES, 0);
LocalSpatialAudioConfig localSpatialAudioConfig = new LocalSpatialAudioConfig();
localSpatialAudioConfig.mRtcEngine = engine;
// 创建 ILocalSpatialAudioEngine
localSpatial = ILocalSpatialAudioEngine.create();
// 初始化 ILocalSpatialAudioEngine
localSpatial.initialize(localSpatialAudioConfig);
// 设置音频接收范围(米)
localSpatial.setAudioRecvRange(50);
// 设置游戏引擎单位距离的长度(米)
localSpatial.setDistanceUnit(1);
// 更新本地用户的空间位置
float[] pos = new float[]{0.0F, 0.0F, 0.0F};
float[] forward = new float[]{1.0F, 0.0F, 0.0F};
float[] right = new float[]{0.0F, 1.0F, 0.0F};
float[] up = new float[]{0.0F, 0.0F, 1.0F};
localSpatial.updateSelfPosition(pos, forward, right, up);
}
private void updateSpatialSoundParam() {
float transX = speakerIv.getTranslationX();
float transY = speakerIv.getTranslationY();
double viewDistance = Math.sqrt(Math.pow(transX, 2) + Math.pow(transY, 2));
double viewMaxDistance = Math.sqrt(Math.pow((rootView.getWidth() - speakerIv.getWidth()) / 2.0f, 2) + Math.pow((rootView.getHeight() - speakerIv.getHeight()) / 2.0f, 2));
double spkMaxDistance = 3;
double spkMinDistance = 1;
double spkDistance = spkMaxDistance * (viewDistance / viewMaxDistance);
if (spkDistance < spkMinDistance) {
spkDistance = spkMinDistance;
}
if (spkDistance > spkMaxDistance) {
spkDistance = spkMaxDistance;
}
double degree = getDegree((int) transX, (int) transY);
if (transX > 0) {
degree = 360 - degree;
}
double posForward = spkDistance * Math.cos(degree);
double posRight = spkDistance * Math.sin(degree);
// 更新媒体播放器的空间位置
RemoteVoicePositionInfo positionInfo = new RemoteVoicePositionInfo();
positionInfo.forward = new float[]{1.0F, 0.0F, 0.0F};
positionInfo.position = new float[]{(float) posForward, (float) posRight, 0.0F};
localSpatial.updatePlayerPositionInfo(mediaPlayer.getMediaPlayerId(), positionInfo);
}
private int getDegree(int point1X, int point1Y) {
int vertexPointX = 0, vertexPointY = 0, point0X = 0;
int point0Y = -10;
int vector = (point0X - vertexPointX) * (point1X - vertexPointX) + (point0Y - vertexPointY) * (point1Y - vertexPointY);
double sqrt = Math.sqrt(
(Math.abs((point0X - vertexPointX) * (point0X - vertexPointX)) + Math.abs((point0Y - vertexPointY) * (point0Y - vertexPointY)))
* (Math.abs((point1X - vertexPointX) * (point1X - vertexPointX)) + Math.abs((point1Y - vertexPointY) * (point1Y - vertexPointY)))
);
double radian = Math.acos(vector / sqrt);
return (int) (180 * radian / Math.PI);
}
@Override
public void onDestroy() {
super.onDestroy();
// 停止播放媒体资源
mediaPlayer.stop();
handler.removeCallbacksAndMessages(null);
handler.post(RtcEngine::destroy);
engine = null;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Context context = getContext();
if (context == null) {
return;
}
try {
String appId = getString(R.string.agora_app_id);
// 创建并初始化 RtcEngine
engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler);
// 创建 IMediaPlayer
mediaPlayer = engine.createMediaPlayer();
// 注册一个播放观测器
mediaPlayer.registerPlayerObserver(iMediaPlayerObserver);
} catch (Exception e) {
e.printStackTrace();
getActivity().onBackPressed();
}
}
private IMediaPlayerObserver iMediaPlayerObserver = new IMediaPlayerObserver() {
@Override
// 报告播放器状态改变
public void onPlayerStateChanged(io.agora.mediaplayer.Constants.MediaPlayerState mediaPlayerState, io.agora.mediaplayer.Constants.MediaPlayerError mediaPlayerError) {
Log.e(TAG, "onPlayerStateChanged mediaPlayerState " + mediaPlayerState);
if (mediaPlayerState.equals(PLAYER_STATE_OPEN_COMPLETED)) {
// 设置循环播放媒体资源
mediaPlayer.setLoopCount(-1);
// 播放媒体资源
mediaPlayer.play();
}
}
};
}
相关文档
示例项目
声网在 GitHub 上提供开源的示例项目 SpatialSound。你可以前往下载,或查看其中的源代码。