diff --git a/api/src/main/java/com/bartlomiejpluta/base/api/audio/Listener.java b/api/src/main/java/com/bartlomiejpluta/base/api/audio/Listener.java new file mode 100644 index 00000000..4233ac36 --- /dev/null +++ b/api/src/main/java/com/bartlomiejpluta/base/api/audio/Listener.java @@ -0,0 +1,12 @@ +package com.bartlomiejpluta.base.api.audio; + +import org.joml.Vector3fc; + +public interface Listener { + + void setPosition(Vector3fc position); + + void setSpeed(Vector3fc speed); + + void setOrientation(Vector3fc at, Vector3fc up); +} diff --git a/api/src/main/java/com/bartlomiejpluta/base/api/audio/Sound.java b/api/src/main/java/com/bartlomiejpluta/base/api/audio/Sound.java new file mode 100644 index 00000000..a0aa86ce --- /dev/null +++ b/api/src/main/java/com/bartlomiejpluta/base/api/audio/Sound.java @@ -0,0 +1,23 @@ +package com.bartlomiejpluta.base.api.audio; + +import org.joml.Vector3fc; + +public interface Sound { + void play(); + + void pause(); + + void stop(); + + boolean isPlaying(); + + void setGain(float gain); + + void setRepeat(boolean repeat); + + void setRelative(boolean relative); + + void setPosition(Vector3fc position); + + void setSpeed(Vector3fc speed); +} diff --git a/api/src/main/java/com/bartlomiejpluta/base/api/context/Context.java b/api/src/main/java/com/bartlomiejpluta/base/api/context/Context.java index dccc2911..1cbee639 100644 --- a/api/src/main/java/com/bartlomiejpluta/base/api/context/Context.java +++ b/api/src/main/java/com/bartlomiejpluta/base/api/context/Context.java @@ -1,6 +1,7 @@ package com.bartlomiejpluta.base.api.context; import com.bartlomiejpluta.base.api.animation.Animation; +import com.bartlomiejpluta.base.api.audio.Sound; import com.bartlomiejpluta.base.api.camera.Camera; import com.bartlomiejpluta.base.api.entity.Entity; import com.bartlomiejpluta.base.api.gui.GUI; @@ -35,6 +36,14 @@ public interface Context extends Updatable, Renderable, Disposable { GUI newGUI(); + Sound createSound(String soundUid); + + void disposeSound(Sound sound); + + void playSound(String soundUid); + + void playSound(String soundUid, float gain); + boolean isRunning(); void close(); diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/asset/SoundAsset.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/asset/SoundAsset.java new file mode 100644 index 00000000..0c5a8e7e --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/asset/SoundAsset.java @@ -0,0 +1,10 @@ +package com.bartlomiejpluta.base.engine.audio.asset; + +import com.bartlomiejpluta.base.engine.common.asset.Asset; +import lombok.NonNull; + +public class SoundAsset extends Asset { + public SoundAsset(@NonNull String uid, @NonNull String source) { + super(uid, source); + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/manager/DefaultSoundManager.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/manager/DefaultSoundManager.java new file mode 100644 index 00000000..38a852ee --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/manager/DefaultSoundManager.java @@ -0,0 +1,59 @@ +package com.bartlomiejpluta.base.engine.audio.manager; + +import com.bartlomiejpluta.base.api.audio.Sound; +import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset; +import com.bartlomiejpluta.base.engine.core.al.engine.AudioEngine; +import com.bartlomiejpluta.base.engine.core.al.source.AudioSource; +import com.bartlomiejpluta.base.engine.error.AppException; +import com.bartlomiejpluta.base.engine.project.config.ProjectConfiguration; +import com.bartlomiejpluta.base.engine.util.res.ResourcesManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@Slf4j +@Component +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) +public class DefaultSoundManager implements SoundManager { + private final Map assets = new HashMap<>(); + private final Set loadedBuffers = new HashSet<>(); + private final AudioEngine engine; + private final ResourcesManager resourcesManager; + private final ProjectConfiguration configuration; + + @Override + public void registerAsset(SoundAsset asset) { + log.info("Registering [{}] sound asset under UID: [{}]", asset.getSource(), asset.getUid()); + assets.put(asset.getUid(), asset); + } + + @Override + public Sound loadObject(String uid) { + if (!loadedBuffers.contains(uid)) { + log.info("Loading [{}] sound", uid); + var asset = assets.get(uid); + + if (asset == null) { + throw new AppException("The sound asset with UID: [%s] does not exist", uid); + } + + var source = configuration.projectFile("audio", asset.getSource()); + var buffer = resourcesManager.loadResourceAsByteBuffer(source); + engine.loadVorbis(asset.getUid(), buffer); + loadedBuffers.add(uid); + } + + return engine.createSource(uid); + } + + @Override + public void disposeSound(Sound sound) { + engine.disposeSource((AudioSource) sound); + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/manager/SoundManager.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/manager/SoundManager.java new file mode 100644 index 00000000..58e54c36 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/audio/manager/SoundManager.java @@ -0,0 +1,9 @@ +package com.bartlomiejpluta.base.engine.audio.manager; + +import com.bartlomiejpluta.base.api.audio.Sound; +import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset; +import com.bartlomiejpluta.base.engine.common.manager.AssetManager; + +public interface SoundManager extends AssetManager { + void disposeSound(Sound sound); +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/context/manager/DefaultContextManager.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/context/manager/DefaultContextManager.java index 6eb5c9f6..2a500abd 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/context/manager/DefaultContextManager.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/context/manager/DefaultContextManager.java @@ -2,6 +2,7 @@ package com.bartlomiejpluta.base.engine.context.manager; import com.bartlomiejpluta.base.api.context.Context; import com.bartlomiejpluta.base.api.runner.GameRunner; +import com.bartlomiejpluta.base.engine.audio.manager.SoundManager; import com.bartlomiejpluta.base.engine.context.model.DefaultContext; import com.bartlomiejpluta.base.engine.core.engine.GameEngine; import com.bartlomiejpluta.base.engine.gui.manager.FontManager; @@ -39,6 +40,7 @@ public class DefaultContextManager implements ContextManager { private final ClassLoader classLoader; private final Inflater inflater; private final WidgetDefinitionManager widgetDefinitionManager; + private final SoundManager soundManager; @SneakyThrows @Override @@ -55,6 +57,7 @@ public class DefaultContextManager implements ContextManager { project.getAnimationAssets().forEach(animationManager::registerAsset); project.getFontAssets().forEach(fontManager::registerAsset); project.getWidgetDefinitionAssets().forEach(widgetDefinitionManager::registerAsset); + project.getSoundAssets().forEach(soundManager::registerAsset); log.info("Creating game runner instance"); var runnerClass = classLoader.loadClass(project.getRunner()); @@ -70,6 +73,7 @@ public class DefaultContextManager implements ContextManager { .fontManager(fontManager) .inflater(inflater) .widgetDefinitionManager(widgetDefinitionManager) + .soundManager(soundManager) .gameRunner(gameRunner) .projectName(project.getName()) .build(); diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/context/model/DefaultContext.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/context/model/DefaultContext.java index 8cc9073c..ba2429c3 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/context/model/DefaultContext.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/context/model/DefaultContext.java @@ -1,6 +1,7 @@ package com.bartlomiejpluta.base.engine.context.model; import com.bartlomiejpluta.base.api.animation.Animation; +import com.bartlomiejpluta.base.api.audio.Sound; import com.bartlomiejpluta.base.api.camera.Camera; import com.bartlomiejpluta.base.api.context.Context; import com.bartlomiejpluta.base.api.entity.Entity; @@ -10,6 +11,7 @@ import com.bartlomiejpluta.base.api.input.Input; import com.bartlomiejpluta.base.api.map.handler.MapHandler; import com.bartlomiejpluta.base.api.runner.GameRunner; import com.bartlomiejpluta.base.api.screen.Screen; +import com.bartlomiejpluta.base.engine.audio.manager.SoundManager; import com.bartlomiejpluta.base.engine.core.engine.GameEngine; import com.bartlomiejpluta.base.engine.gui.manager.FontManager; import com.bartlomiejpluta.base.engine.gui.manager.WidgetDefinitionManager; @@ -58,6 +60,9 @@ public class DefaultContext implements Context { @NonNull private final WidgetDefinitionManager widgetDefinitionManager; + @NonNull + private final SoundManager soundManager; + @Getter @NonNull private final GameRunner gameRunner; @@ -80,6 +85,8 @@ public class DefaultContext implements Context { private final List guis = new LinkedList<>(); + private final List sounds = new LinkedList<>(); + @SneakyThrows @Override public void init(@NonNull Screen screen, @NonNull Input input, @NonNull Camera camera) { @@ -133,6 +140,31 @@ public class DefaultContext implements Context { return gui; } + @Override + public Sound createSound(String soundUid) { + return soundManager.loadObject(soundUid); + } + + @Override + public void disposeSound(Sound sound) { + soundManager.disposeSound(sound); + } + + @Override + public void playSound(String soundUid) { + var sound = soundManager.loadObject(soundUid); + sounds.add(sound); + sound.play(); + } + + @Override + public void playSound(String soundUid, float gain) { + var sound = soundManager.loadObject(soundUid); + sound.setGain(gain); + sounds.add(sound); + sound.play(); + } + @Override public boolean isRunning() { return engine.isRunning(); @@ -183,6 +215,14 @@ public class DefaultContext implements Context { if (map != null) { map.update(dt); } + + for (var iterator = sounds.iterator(); iterator.hasNext(); ) { + var sound = iterator.next(); + if (!sound.isPlaying()) { + iterator.remove(); + soundManager.disposeSound(sound); + } + } } @Override diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/buffer/AudioBuffer.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/buffer/AudioBuffer.java new file mode 100644 index 00000000..104d2a5c --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/buffer/AudioBuffer.java @@ -0,0 +1,63 @@ +package com.bartlomiejpluta.base.engine.core.al.buffer; + +import com.bartlomiejpluta.base.engine.error.AppException; +import com.bartlomiejpluta.base.internal.gc.Disposable; +import lombok.Getter; +import org.lwjgl.stb.STBVorbisInfo; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; + +import static org.lwjgl.openal.AL10.*; +import static org.lwjgl.stb.STBVorbis.*; +import static org.lwjgl.system.MemoryUtil.NULL; + + +public class AudioBuffer implements Disposable { + private final ShortBuffer pcm; + + @Getter + private final int id; + + public AudioBuffer(ByteBuffer buffer) { + id = alGenBuffers(); + try (var info = STBVorbisInfo.malloc()) { + pcm = readVorbis(buffer, info); + alBufferData(id, info.channels() == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, pcm, info.sample_rate()); + } + } + + private ShortBuffer readVorbis(ByteBuffer vorbis, STBVorbisInfo info) { + try (MemoryStack stack = MemoryStack.stackPush()) { + var error = stack.mallocInt(1); + var decoder = stb_vorbis_open_memory(vorbis, error, null); + + if (decoder == NULL) { + throw new AppException("Failed to open OGG Vorbis file: " + error.get(0)); + } + + stb_vorbis_get_info(decoder, info); + + int channels = info.channels(); + + int lengthSamples = stb_vorbis_stream_length_in_samples(decoder); + + var pcm = MemoryUtil.memAllocShort(lengthSamples); + + pcm.limit(stb_vorbis_get_samples_short_interleaved(decoder, channels, pcm) * channels); + stb_vorbis_close(decoder); + + return pcm; + } + } + + @Override + public void dispose() { + alDeleteBuffers(id); + if (pcm != null) { + MemoryUtil.memFree(pcm); + } + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/engine/AudioEngine.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/engine/AudioEngine.java new file mode 100644 index 00000000..89961c24 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/engine/AudioEngine.java @@ -0,0 +1,18 @@ +package com.bartlomiejpluta.base.engine.core.al.engine; + +import com.bartlomiejpluta.base.engine.common.init.Initianizable; +import com.bartlomiejpluta.base.engine.core.al.listener.AudioListener; +import com.bartlomiejpluta.base.engine.core.al.source.AudioSource; +import com.bartlomiejpluta.base.internal.gc.Cleanable; + +import java.nio.ByteBuffer; + +public interface AudioEngine extends Initianizable, Cleanable { + AudioListener getListener(); + + void loadVorbis(String name, ByteBuffer vorbis); + + AudioSource createSource(String name); + + void disposeSource(AudioSource source); +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/engine/DefaultAudioEngine.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/engine/DefaultAudioEngine.java new file mode 100644 index 00000000..abed9978 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/engine/DefaultAudioEngine.java @@ -0,0 +1,112 @@ +package com.bartlomiejpluta.base.engine.core.al.engine; + +import com.bartlomiejpluta.base.engine.core.al.buffer.AudioBuffer; +import com.bartlomiejpluta.base.engine.core.al.listener.AudioListener; +import com.bartlomiejpluta.base.engine.core.al.source.AudioSource; +import com.bartlomiejpluta.base.engine.error.AppException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.lwjgl.openal.AL; +import org.lwjgl.openal.ALC; +import org.springframework.stereotype.Component; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static org.lwjgl.openal.ALC10.*; +import static org.lwjgl.openal.ALC11.alcOpenDevice; +import static org.lwjgl.system.MemoryUtil.NULL; + +@Slf4j +@Component +public class DefaultAudioEngine implements AudioEngine { + private final Map buffers = new HashMap<>(); + private final List sources = new LinkedList<>(); + + private long device; + private long context; + + @Getter + private AudioListener listener; + + @Override + public void init() { + log.info("Initializing default audio device"); + device = alcOpenDevice((ByteBuffer) null); + + if (device == NULL) { + throw new AppException("Failed to open the default OpenAL device"); + } + + log.info("Initializing audio context"); + var deviceCapabilities = ALC.createCapabilities(device); + context = alcCreateContext(device, (IntBuffer) null); + + if (context == NULL) { + throw new AppException("Failed to create OpenAL context"); + } + + alcMakeContextCurrent(context); + AL.createCapabilities(deviceCapabilities); + + log.info("Initializing audio listener"); + listener = new AudioListener(); + } + + @Override + public void loadVorbis(String name, ByteBuffer vorbis) { + var buffer = new AudioBuffer(vorbis); + buffers.put(name, buffer); + } + + @Override + public AudioSource createSource(String name) { + var buffer = buffers.get(name); + + if (buffer == null) { + throw new AppException("Audio buffer with name [%s] does not exist", name); + } + + var source = new AudioSource(); + source.setBuffer(buffer); + + sources.add(source); + + return source; + } + + @Override + public void disposeSource(AudioSource source) { + source.dispose(); + sources.remove(source); + } + + @Override + public void cleanUp() { + log.info("Disposing audio sources"); + sources.forEach(AudioSource::dispose); + log.info("{} audio sources have been disposed", sources.size()); + + log.info("Disposing audio buffers"); + buffers.forEach((name, buffer) -> buffer.dispose()); + log.info("{} audio buffers have been disposed", buffers.size()); + + if (context != NULL) { + log.info("Disposing audio context"); + alcDestroyContext(context); + } else { + log.warn("Audio context is NULL and will not be disposed"); + } + + if (device != NULL) { + log.info("Closing audio device"); + alcCloseDevice(device); + } else { + log.warn("Audio device is NULL and will not be closed"); + } + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/listener/AudioListener.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/listener/AudioListener.java new file mode 100644 index 00000000..8ed79aa4 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/listener/AudioListener.java @@ -0,0 +1,42 @@ +package com.bartlomiejpluta.base.engine.core.al.listener; + +import com.bartlomiejpluta.base.api.audio.Listener; +import org.joml.Vector3f; +import org.joml.Vector3fc; + +import static org.lwjgl.openal.AL10.*; +import static org.lwjgl.openal.AL11.alListener3f; + +public class AudioListener implements Listener { + + public AudioListener() { + this(new Vector3f(0, 0, 0)); + } + + public AudioListener(Vector3fc position) { + alListener3f(AL_POSITION, position.x(), position.y(), position.z()); + alListener3f(AL_VELOCITY, 0, 0, 0); + } + + @Override + public void setPosition(Vector3fc position) { + alListener3f(AL_POSITION, position.x(), position.y(), position.z()); + } + + @Override + public void setSpeed(Vector3fc speed) { + alListener3f(AL_VELOCITY, speed.x(), speed.y(), speed.z()); + } + + @Override + public void setOrientation(Vector3fc at, Vector3fc up) { + var data = new float[6]; + data[0] = at.x(); + data[1] = at.y(); + data[2] = at.z(); + data[3] = up.x(); + data[4] = up.y(); + data[5] = up.z(); + alListenerfv(AL_ORIENTATION, data); + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/source/AudioSource.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/source/AudioSource.java new file mode 100644 index 00000000..2284e002 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/al/source/AudioSource.java @@ -0,0 +1,73 @@ +package com.bartlomiejpluta.base.engine.core.al.source; + +import com.bartlomiejpluta.base.api.audio.Sound; +import com.bartlomiejpluta.base.engine.core.al.buffer.AudioBuffer; +import com.bartlomiejpluta.base.internal.gc.Disposable; +import org.joml.Vector3fc; + +import static org.lwjgl.openal.AL10.*; +import static org.lwjgl.openal.AL11.alGenSources; + +public class AudioSource implements Sound, Disposable { + private final int id = alGenSources(); + + public void setBuffer(AudioBuffer buffer) { + stop(); + alSourcei(id, AL_BUFFER, buffer.getId()); + } + + public void setParameter(int param, float value) { + alSourcef(id, param, value); + } + + @Override + public void setPosition(Vector3fc position) { + alSource3f(id, AL_POSITION, position.x(), position.y(), position.z()); + } + + @Override + public void setSpeed(Vector3fc speed) { + alSource3f(id, AL_VELOCITY, speed.x(), speed.y(), speed.z()); + } + + @Override + public void setGain(float gain) { + alSourcef(id, AL_GAIN, gain); + } + + @Override + public void play() { + alSourcePlay(id); + } + + @Override + public void pause() { + alSourcePause(id); + } + + @Override + public void stop() { + alSourceStop(id); + } + + @Override + public boolean isPlaying() { + return alGetSourcei(id, AL_SOURCE_STATE) == AL_PLAYING; + } + + @Override + public void setRepeat(boolean repeat) { + alSourcei(id, AL_LOOPING, repeat ? AL_TRUE : AL_FALSE); + } + + @Override + public void setRelative(boolean relative) { + alSourcei(id, AL_SOURCE_RELATIVE, relative ? AL_TRUE : AL_FALSE); + } + + @Override + public void dispose() { + stop(); + alDeleteSources(id); + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/project/model/Project.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/project/model/Project.java index ebfe9925..72e029e1 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/project/model/Project.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/project/model/Project.java @@ -1,5 +1,6 @@ package com.bartlomiejpluta.base.engine.project.model; +import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset; import com.bartlomiejpluta.base.engine.gui.asset.FontAsset; import com.bartlomiejpluta.base.engine.gui.asset.WidgetDefinitionAsset; import com.bartlomiejpluta.base.engine.world.animation.asset.AnimationAsset; @@ -43,4 +44,7 @@ public class Project { @NonNull private final List widgetDefinitionAssets; + + @NonNull + private final List soundAssets; } diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/project/serial/ProtobufProjectDeserializer.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/project/serial/ProtobufProjectDeserializer.java index 0ca83a11..fb0d4513 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/project/serial/ProtobufProjectDeserializer.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/project/serial/ProtobufProjectDeserializer.java @@ -1,5 +1,6 @@ package com.bartlomiejpluta.base.engine.project.serial; +import com.bartlomiejpluta.base.engine.audio.asset.SoundAsset; import com.bartlomiejpluta.base.engine.gui.asset.FontAsset; import com.bartlomiejpluta.base.engine.gui.asset.WidgetDefinitionAsset; import com.bartlomiejpluta.base.engine.project.model.Project; @@ -32,6 +33,7 @@ public class ProtobufProjectDeserializer extends ProjectDeserializer { .animationAssets(proto.getAnimationsList().stream().map(this::parseAnimationAsset).collect(toList())) .fontAssets(proto.getFontsList().stream().map(this::parseFontAsset).collect(toList())) .widgetDefinitionAssets(proto.getWidgetsList().stream().map(this::parseWidgetAsset).collect(toList())) + .soundAssets(proto.getSoundsList().stream().map(this::parseSoundAsset).collect(toList())) .build(); } @@ -62,4 +64,8 @@ public class ProtobufProjectDeserializer extends ProjectDeserializer { private AnimationAsset parseAnimationAsset(ProjectProto.AnimationAsset proto) { return new AnimationAsset(proto.getUid(), proto.getSource(), proto.getRows(), proto.getColumns()); } + + private SoundAsset parseSoundAsset(ProjectProto.SoundAsset proto) { + return new SoundAsset(proto.getUid(), proto.getSource()); + } }