more work on Voice
This commit is contained in:
parent
40223a5090
commit
f9e8b5badf
@ -437,6 +437,7 @@ SET (STAR_EXT_LIBS ${STAR_EXT_LIBS}
|
|||||||
${FREETYPE_LIBRARY}
|
${FREETYPE_LIBRARY}
|
||||||
${PNG_LIBRARY}
|
${PNG_LIBRARY}
|
||||||
${ZLIB_LIBRARY}
|
${ZLIB_LIBRARY}
|
||||||
|
"extern/opus/opus"
|
||||||
)
|
)
|
||||||
|
|
||||||
IF (STAR_BUILD_GUI)
|
IF (STAR_BUILD_GUI)
|
||||||
|
2
source/extern/CMakeLists.txt
vendored
2
source/extern/CMakeLists.txt
vendored
@ -79,4 +79,4 @@ SET (star_extern_SOURCES
|
|||||||
)
|
)
|
||||||
|
|
||||||
ADD_LIBRARY (star_extern OBJECT ${star_extern_SOURCES} ${star_extern_HEADERS})
|
ADD_LIBRARY (star_extern OBJECT ${star_extern_SOURCES} ${star_extern_HEADERS})
|
||||||
TARGET_LINK_LIBRARIES(star_extern opus)
|
TARGET_LINK_LIBRARIES(star_extern PUBLIC opus)
|
@ -7,6 +7,7 @@
|
|||||||
#include "StarAssets.hpp"
|
#include "StarAssets.hpp"
|
||||||
#include "StarWorldClient.hpp"
|
#include "StarWorldClient.hpp"
|
||||||
#include "StarWorldPainter.hpp"
|
#include "StarWorldPainter.hpp"
|
||||||
|
#include "StarVoice.hpp"
|
||||||
|
|
||||||
namespace Star {
|
namespace Star {
|
||||||
|
|
||||||
@ -80,34 +81,39 @@ void MainMixer::update(bool muteSfx, bool muteMusic) {
|
|||||||
auto cameraPos = m_worldPainter->camera().centerWorldPosition();
|
auto cameraPos = m_worldPainter->camera().centerWorldPosition();
|
||||||
auto worldGeometry = currentWorld->geometry();
|
auto worldGeometry = currentWorld->geometry();
|
||||||
|
|
||||||
m_mixer->update([&](unsigned channel, Vec2F pos, float rangeMultiplier) {
|
Mixer::PositionalAttenuationFunction attenuationFunction = [&](unsigned channel, Vec2F pos, float rangeMultiplier) {
|
||||||
Vec2F playerDiff = worldGeometry.diff(pos, playerPos);
|
Vec2F playerDiff = worldGeometry.diff(pos, playerPos);
|
||||||
Vec2F cameraDiff = worldGeometry.diff(pos, cameraPos);
|
Vec2F cameraDiff = worldGeometry.diff(pos, cameraPos);
|
||||||
float playerMagSq = playerDiff.magnitudeSquared();
|
float playerMagSq = playerDiff.magnitudeSquared();
|
||||||
float cameraMagSq = cameraDiff.magnitudeSquared();
|
float cameraMagSq = cameraDiff.magnitudeSquared();
|
||||||
|
|
||||||
Vec2F diff;
|
Vec2F diff;
|
||||||
float diffMagnitude;
|
float diffMagnitude;
|
||||||
if (playerMagSq < cameraMagSq) {
|
if (playerMagSq < cameraMagSq) {
|
||||||
diff = playerDiff;
|
diff = playerDiff;
|
||||||
diffMagnitude = sqrt(playerMagSq);
|
diffMagnitude = sqrt(playerMagSq);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
diff = cameraDiff;
|
diff = cameraDiff;
|
||||||
diffMagnitude = sqrt(cameraMagSq);
|
diffMagnitude = sqrt(cameraMagSq);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diffMagnitude == 0.0f)
|
if (diffMagnitude == 0.0f)
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
|
|
||||||
Vec2F diffNorm = diff / diffMagnitude;
|
Vec2F diffNorm = diff / diffMagnitude;
|
||||||
|
|
||||||
float stereoIncidence = channel == 0 ? -diffNorm[0] : diffNorm[0];
|
float stereoIncidence = channel == 0 ? -diffNorm[0] : diffNorm[0];
|
||||||
|
|
||||||
float maxDistance = baseMaxDistance * rangeMultiplier * lerp((stereoIncidence + 1.0f) / 2.0f, stereoAdjustmentRange[0], stereoAdjustmentRange[1]);
|
float maxDistance = baseMaxDistance * rangeMultiplier * lerp((stereoIncidence + 1.0f) / 2.0f, stereoAdjustmentRange[0], stereoAdjustmentRange[1]);
|
||||||
|
|
||||||
return pow(clamp(diffMagnitude / maxDistance, 0.0f, 1.0f), 1.0f / attenuationGamma);
|
return pow(clamp(diffMagnitude / maxDistance, 0.0f, 1.0f), 1.0f / attenuationGamma);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (Voice* voice = Voice::singletonPtr())
|
||||||
|
voice->update(attenuationFunction);
|
||||||
|
|
||||||
|
m_mixer->update(attenuationFunction);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (m_mixer->hasEffect("lowpass"))
|
if (m_mixer->hasEffect("lowpass"))
|
||||||
|
@ -1,11 +1,35 @@
|
|||||||
#include "StarVoice.hpp"
|
#include "StarVoice.hpp"
|
||||||
|
#include "StarFormat.hpp"
|
||||||
|
#include "opus/include/opus.h"
|
||||||
|
|
||||||
|
#include "SDL.h"
|
||||||
|
|
||||||
namespace Star {
|
namespace Star {
|
||||||
|
|
||||||
STAR_EXCEPTION(VoiceException, StarException);
|
EnumMap<VoiceTriggerMode> const VoiceTriggerModeNames{
|
||||||
|
{VoiceTriggerMode::VoiceActivity, "VoiceActivity"},
|
||||||
|
{VoiceTriggerMode::PushToTalk, "PushToTalk"}
|
||||||
|
};
|
||||||
|
|
||||||
void Voice::mix(int16_t* buffer, size_t frames, unsigned channels) {
|
EnumMap<VoiceChannelMode> const VoiceChannelModeNames{
|
||||||
|
{VoiceChannelMode::Mono, "Mono"},
|
||||||
|
{VoiceChannelMode::Stereo, "Stereo"}
|
||||||
|
};
|
||||||
|
|
||||||
|
static SDL_AudioDeviceID sdlInputDevice = 0;
|
||||||
|
|
||||||
|
constexpr int VOICE_SAMPLE_RATE = 48000;
|
||||||
|
constexpr int VOICE_FRAME_SIZE = 960;
|
||||||
|
|
||||||
|
constexpr int VOICE_MAX_FRAME_SIZE = 6 * VOICE_FRAME_SIZE;
|
||||||
|
constexpr int VOICE_MAX_PACKET_SIZE = 3 * 1276;
|
||||||
|
|
||||||
|
constexpr uint16_t VOICE_VERSION = 1;
|
||||||
|
|
||||||
|
Voice::Speaker::Speaker(SpeakerId id)
|
||||||
|
: decoderMono (createDecoder(1), opus_decoder_destroy)
|
||||||
|
, decoderStereo(createDecoder(2), opus_decoder_destroy) {
|
||||||
|
speakerId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Voice* Voice::s_singleton;
|
Voice* Voice::s_singleton;
|
||||||
@ -21,10 +45,15 @@ Voice& Voice::singleton() {
|
|||||||
return *s_singleton;
|
return *s_singleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
Voice::Voice() {
|
Voice::Voice() : m_encoder(nullptr, opus_encoder_destroy) {
|
||||||
if (s_singleton)
|
if (s_singleton)
|
||||||
throw VoiceException("Singleton Voice has been constructed twice");
|
throw VoiceException("Singleton Voice has been constructed twice");
|
||||||
|
|
||||||
|
m_clientSpeaker = make_shared<Speaker>(m_speakerId);
|
||||||
|
m_triggerMode = VoiceTriggerMode::PushToTalk;
|
||||||
|
m_channelMode = VoiceChannelMode::Mono;
|
||||||
|
|
||||||
|
resetEncoder();
|
||||||
s_singleton = this;
|
s_singleton = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,4 +61,71 @@ Voice::~Voice() {
|
|||||||
s_singleton = nullptr;
|
s_singleton = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Voice::load(Json const& config) {
|
||||||
|
// do stuff
|
||||||
|
}
|
||||||
|
Json Voice::save() const {
|
||||||
|
return JsonObject{};
|
||||||
|
}
|
||||||
|
|
||||||
|
Voice::SpeakerPtr Voice::setLocalSpeaker(SpeakerId speakerId) {
|
||||||
|
if (m_speakers.contains(m_speakerId))
|
||||||
|
m_speakers.remove(m_speakerId);
|
||||||
|
|
||||||
|
m_clientSpeaker->speakerId = m_speakerId = speakerId;
|
||||||
|
return m_speakers.insert(m_speakerId, m_clientSpeaker).first->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
Voice::SpeakerPtr Voice::speaker(SpeakerId speakerId) {
|
||||||
|
if (m_speakerId == speakerId)
|
||||||
|
return m_clientSpeaker;
|
||||||
|
else {
|
||||||
|
if (SpeakerPtr const* ptr = m_speakers.ptr(speakerId))
|
||||||
|
return *ptr;
|
||||||
|
else
|
||||||
|
return m_speakers.emplace(speakerId, make_shared<Speaker>(speakerId)).first->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::mix(int16_t* buffer, size_t frames, unsigned channels) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::update(PositionalAttenuationFunction positionalAttenuationFunction) {
|
||||||
|
if (positionalAttenuationFunction) {
|
||||||
|
for (auto& entry : m_speakers) {
|
||||||
|
if (SpeakerPtr& speaker = entry.second) {
|
||||||
|
speaker->channelVolumes = {
|
||||||
|
positionalAttenuationFunction(0, speaker->position, 1.0f),
|
||||||
|
positionalAttenuationFunction(1, speaker->position, 1.0f)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusDecoder* Voice::createDecoder(int channels) {
|
||||||
|
int error;
|
||||||
|
OpusDecoder* decoder = opus_decoder_create(VOICE_SAMPLE_RATE, channels, &error);
|
||||||
|
if (error != OPUS_OK)
|
||||||
|
throw VoiceException::format("Could not create decoder: {}", opus_strerror(error));
|
||||||
|
else
|
||||||
|
return decoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpusEncoder* Voice::createEncoder(int channels) {
|
||||||
|
int error;
|
||||||
|
OpusEncoder* encoder = opus_encoder_create(VOICE_SAMPLE_RATE, channels, OPUS_APPLICATION_AUDIO, &error);
|
||||||
|
if (error != OPUS_OK)
|
||||||
|
throw VoiceException::format("Could not create encoder: {}", opus_strerror(error));
|
||||||
|
else
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Voice::resetEncoder() {
|
||||||
|
int channels = encoderChannels();
|
||||||
|
m_encoder.reset(createEncoder(channels));
|
||||||
|
opus_encoder_ctl(m_encoder.get(), OPUS_SET_BITRATE(channels == 2 ? 50000 : 24000));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,31 +1,98 @@
|
|||||||
#ifndef STAR_VOICE_HPP
|
#ifndef STAR_VOICE_HPP
|
||||||
#define STAR_VOICE_HPP
|
#define STAR_VOICE_HPP
|
||||||
#include "StarString.hpp"
|
#include "StarJson.hpp"
|
||||||
|
#include "StarBiMap.hpp"
|
||||||
|
#include "StarGameTypes.hpp"
|
||||||
|
#include "StarException.hpp"
|
||||||
|
|
||||||
|
struct OpusDecoder;
|
||||||
|
typedef std::unique_ptr<OpusDecoder, void(*)(OpusDecoder*)> OpusDecoderPtr;
|
||||||
|
struct OpusEncoder;
|
||||||
|
typedef std::unique_ptr<OpusEncoder, void(*)(OpusEncoder*)> OpusEncoderPtr;
|
||||||
|
|
||||||
namespace Star {
|
namespace Star {
|
||||||
|
|
||||||
STAR_CLASS(Voice);
|
STAR_EXCEPTION(VoiceException, StarException);
|
||||||
|
|
||||||
class Voice {
|
enum class VoiceTriggerMode : uint8_t { VoiceActivity, PushToTalk };
|
||||||
public:
|
extern EnumMap<VoiceTriggerMode> const VoiceTriggerModeNames;
|
||||||
void mix(int16_t* buffer, size_t frames, unsigned channels);
|
|
||||||
|
|
||||||
// Get pointer to the singleton Voice instance, if it exists. Otherwise,
|
enum class VoiceChannelMode: uint8_t { Mono = 1, Stereo = 2 };
|
||||||
// returns nullptr.
|
extern EnumMap<VoiceChannelMode> const VoiceChannelModeNames;
|
||||||
static Voice* singletonPtr();
|
|
||||||
|
|
||||||
// Gets reference to Voice singleton, throws VoiceException if root
|
STAR_CLASS(Voice);
|
||||||
// is not initialized.
|
|
||||||
static Voice& singleton();
|
|
||||||
|
|
||||||
Voice();
|
class Voice {
|
||||||
~Voice();
|
public:
|
||||||
|
// Individual speakers are represented by their connection ID.
|
||||||
|
typedef ConnectionId SpeakerId;
|
||||||
|
|
||||||
Voice(Voice const&) = delete;
|
struct Speaker {
|
||||||
Voice& operator=(Voice const&) = delete;
|
SpeakerId speakerId = 0;
|
||||||
private:
|
EntityId entityId = 0;
|
||||||
static Voice* s_singleton;
|
Vec2F position = Vec2F();
|
||||||
|
String name = "Unnamed";
|
||||||
|
|
||||||
|
OpusDecoderPtr decoderMono;
|
||||||
|
OpusDecoderPtr decoderStereo;
|
||||||
|
|
||||||
|
atomic<bool> active = false;
|
||||||
|
atomic<float> currentLoudness = 0.0f;
|
||||||
|
atomic<Array<float, 2>> channelVolumes = Array<float, 2>::filled(1.0f);
|
||||||
|
|
||||||
|
Speaker(SpeakerId speakerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef std::shared_ptr<Speaker> SpeakerPtr;
|
||||||
|
|
||||||
|
// Get pointer to the singleton Voice instance, if it exists. Otherwise,
|
||||||
|
// returns nullptr.
|
||||||
|
static Voice* singletonPtr();
|
||||||
|
|
||||||
|
// Gets reference to Voice singleton, throws VoiceException if root
|
||||||
|
// is not initialized.
|
||||||
|
static Voice& singleton();
|
||||||
|
|
||||||
|
Voice();
|
||||||
|
~Voice();
|
||||||
|
|
||||||
|
Voice(Voice const&) = delete;
|
||||||
|
Voice& operator=(Voice const&) = delete;
|
||||||
|
|
||||||
|
void load(Json const& config);
|
||||||
|
Json save() const;
|
||||||
|
|
||||||
|
// Sets the local speaker ID and returns the local speaker. Must be called upon loading into a world.
|
||||||
|
SpeakerPtr setLocalSpeaker(SpeakerId speakerId);
|
||||||
|
SpeakerPtr speaker(SpeakerId speakerId);
|
||||||
|
|
||||||
|
// Called to mix voice audio with the game.
|
||||||
|
void mix(int16_t* buffer, size_t frames, unsigned channels);
|
||||||
|
|
||||||
|
typedef function<float(unsigned, Vec2F, float)> PositionalAttenuationFunction;
|
||||||
|
void update(PositionalAttenuationFunction positionalAttenuationFunction = {});
|
||||||
|
|
||||||
|
inline int encoderChannels() const {
|
||||||
|
return m_channelMode == VoiceChannelMode::Mono ? 1 : 2;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
static Voice* s_singleton;
|
||||||
|
|
||||||
|
static OpusDecoder* createDecoder(int channels);
|
||||||
|
static OpusEncoder* createEncoder(int channels);
|
||||||
|
void resetEncoder();
|
||||||
|
|
||||||
|
SpeakerId m_speakerId = 0;
|
||||||
|
SpeakerPtr m_clientSpeaker;
|
||||||
|
HashMap<SpeakerId, SpeakerPtr> m_speakers;
|
||||||
|
|
||||||
|
HashSet<SpeakerPtr> m_activeSpeakers;
|
||||||
|
|
||||||
|
OpusEncoderPtr m_encoder;
|
||||||
|
|
||||||
|
VoiceTriggerMode m_triggerMode;
|
||||||
|
VoiceChannelMode m_channelMode;
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user