diff options
Diffstat (limited to 'source/frontend')
-rw-r--r-- | source/frontend/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/frontend/StarMainMixer.cpp | 56 | ||||
-rw-r--r-- | source/frontend/StarMainMixer.hpp | 2 | ||||
-rw-r--r-- | source/frontend/StarVoice.cpp | 146 | ||||
-rw-r--r-- | source/frontend/StarVoice.hpp | 109 |
5 files changed, 289 insertions, 26 deletions
diff --git a/source/frontend/CMakeLists.txt b/source/frontend/CMakeLists.txt index ea6edc6..d8e5390 100644 --- a/source/frontend/CMakeLists.txt +++ b/source/frontend/CMakeLists.txt @@ -56,6 +56,7 @@ SET (star_frontend_HEADERS StarStatusPane.hpp StarTeleportDialog.hpp StarWireInterface.hpp + StarVoice.hpp ) SET (star_frontend_SOURCES @@ -104,6 +105,7 @@ SET (star_frontend_SOURCES StarStatusPane.cpp StarTeleportDialog.cpp StarWireInterface.cpp + StarVoice.cpp ) ADD_LIBRARY (star_frontend OBJECT ${star_frontend_SOURCES} ${star_frontend_HEADERS}) diff --git a/source/frontend/StarMainMixer.cpp b/source/frontend/StarMainMixer.cpp index c49a0fc..6d68ea2 100644 --- a/source/frontend/StarMainMixer.cpp +++ b/source/frontend/StarMainMixer.cpp @@ -7,6 +7,7 @@ #include "StarAssets.hpp" #include "StarWorldClient.hpp" #include "StarWorldPainter.hpp" +#include "StarVoice.hpp" namespace Star { @@ -80,34 +81,39 @@ void MainMixer::update(bool muteSfx, bool muteMusic) { auto cameraPos = m_worldPainter->camera().centerWorldPosition(); auto worldGeometry = currentWorld->geometry(); - m_mixer->update([&](unsigned channel, Vec2F pos, float rangeMultiplier) { - Vec2F playerDiff = worldGeometry.diff(pos, playerPos); - Vec2F cameraDiff = worldGeometry.diff(pos, cameraPos); - float playerMagSq = playerDiff.magnitudeSquared(); - float cameraMagSq = cameraDiff.magnitudeSquared(); - - Vec2F diff; - float diffMagnitude; - if (playerMagSq < cameraMagSq) { - diff = playerDiff; - diffMagnitude = sqrt(playerMagSq); - } - else { - diff = cameraDiff; - diffMagnitude = sqrt(cameraMagSq); - } + Mixer::PositionalAttenuationFunction attenuationFunction = [&](unsigned channel, Vec2F pos, float rangeMultiplier) { + Vec2F playerDiff = worldGeometry.diff(pos, playerPos); + Vec2F cameraDiff = worldGeometry.diff(pos, cameraPos); + float playerMagSq = playerDiff.magnitudeSquared(); + float cameraMagSq = cameraDiff.magnitudeSquared(); + + Vec2F diff; + float diffMagnitude; + if (playerMagSq < cameraMagSq) { + diff = playerDiff; + diffMagnitude = sqrt(playerMagSq); + } + else { + diff = cameraDiff; + diffMagnitude = sqrt(cameraMagSq); + } - if (diffMagnitude == 0.0f) - return 0.0f; + if (diffMagnitude == 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]); + + return pow(clamp(diffMagnitude / maxDistance, 0.0f, 1.0f), 1.0f / attenuationGamma); + }; - float maxDistance = baseMaxDistance * rangeMultiplier * lerp((stereoIncidence + 1.0f) / 2.0f, stereoAdjustmentRange[0], stereoAdjustmentRange[1]); + if (Voice* voice = Voice::singletonPtr()) + voice->update(attenuationFunction); - return pow(clamp(diffMagnitude / maxDistance, 0.0f, 1.0f), 1.0f / attenuationGamma); - }); + m_mixer->update(attenuationFunction); } else { if (m_mixer->hasEffect("lowpass")) @@ -127,8 +133,8 @@ void MainMixer::setVolume(float volume, float rampTime) { m_mixer->setVolume(volume, rampTime); } -void MainMixer::read(int16_t* sampleData, size_t frameCount) { - m_mixer->read(sampleData, frameCount); +void MainMixer::read(int16_t* sampleData, size_t frameCount, Mixer::ExtraMixFunction extraMixFunction) { + m_mixer->read(sampleData, frameCount, extraMixFunction); } } diff --git a/source/frontend/StarMainMixer.hpp b/source/frontend/StarMainMixer.hpp index ecf9443..2582a2f 100644 --- a/source/frontend/StarMainMixer.hpp +++ b/source/frontend/StarMainMixer.hpp @@ -22,7 +22,7 @@ public: MixerPtr mixer() const; void setVolume(float volume, float rampTime = 0.0f); - void read(int16_t* sampleData, size_t frameCount); + void read(int16_t* sampleData, size_t frameCount, Mixer::ExtraMixFunction = {}); private: UniverseClientPtr m_universeClient; diff --git a/source/frontend/StarVoice.cpp b/source/frontend/StarVoice.cpp new file mode 100644 index 0000000..436461b --- /dev/null +++ b/source/frontend/StarVoice.cpp @@ -0,0 +1,146 @@ +#include "StarVoice.hpp" +#include "StarFormat.hpp" +#include "StarApplicationController.hpp" +#include "opus/include/opus.h" + +#include "SDL.h" + +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; + +namespace Star { + +EnumMap<VoiceInputMode> const VoiceInputModeNames{ + {VoiceInputMode::VoiceActivity, "VoiceActivity"}, + {VoiceInputMode::PushToTalk, "PushToTalk"} +}; + +EnumMap<VoiceChannelMode> const VoiceChannelModeNames{ + {VoiceChannelMode::Mono, "Mono"}, + {VoiceChannelMode::Stereo, "Stereo"} +}; + +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::singletonPtr() { + return s_singleton; +} + +Voice& Voice::singleton() { + if (!s_singleton) + throw VoiceException("Voice::singleton() called with no Voice instance available"); + else + return *s_singleton; +} + +Voice::Voice(ApplicationControllerPtr appController) : m_encoder(nullptr, opus_encoder_destroy) { + if (s_singleton) + throw VoiceException("Singleton Voice has been constructed twice"); + + m_clientSpeaker = make_shared<Speaker>(m_speakerId); + m_inputMode = VoiceInputMode::PushToTalk; + m_channelMode = VoiceChannelMode::Mono; + m_applicationController = appController; + + resetEncoder(); + s_singleton = this; +} + +Voice::~Voice() { + 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)); +} + +void Voice::openDevice() { + closeDevice(); + + m_deviceOpen = true; +} + +void Voice::closeDevice() { + if (!m_deviceOpen) + return; + + m_applicationController->closeAudioInputDevice(); + + m_deviceOpen = false; +} + +}
\ No newline at end of file diff --git a/source/frontend/StarVoice.hpp b/source/frontend/StarVoice.hpp new file mode 100644 index 0000000..0d485db --- /dev/null +++ b/source/frontend/StarVoice.hpp @@ -0,0 +1,109 @@ +#ifndef STAR_VOICE_HPP +#define STAR_VOICE_HPP +#include "StarJson.hpp" +#include "StarBiMap.hpp" +#include "StarException.hpp" +#include "StarGameTypes.hpp" +#include "StarMaybe.hpp" +#include "StarApplicationController.hpp" + +struct OpusDecoder; +typedef std::unique_ptr<OpusDecoder, void(*)(OpusDecoder*)> OpusDecoderPtr; +struct OpusEncoder; +typedef std::unique_ptr<OpusEncoder, void(*)(OpusEncoder*)> OpusEncoderPtr; + +namespace Star { + +STAR_EXCEPTION(VoiceException, StarException); + +enum class VoiceInputMode : uint8_t { VoiceActivity, PushToTalk }; +extern EnumMap<VoiceInputMode> const VoiceInputModeNames; + +enum class VoiceChannelMode: uint8_t { Mono = 1, Stereo = 2 }; +extern EnumMap<VoiceChannelMode> const VoiceChannelModeNames; + +STAR_CLASS(Voice); +STAR_CLASS(ApplicationController); + +class Voice { +public: + // Individual speakers are represented by their connection ID. + typedef ConnectionId SpeakerId; + + struct Speaker { + SpeakerId speakerId = 0; + EntityId entityId = 0; + + 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(ApplicationControllerPtr appController); + ~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(); + void openDevice(); + void closeDevice(); + + SpeakerId m_speakerId = 0; + SpeakerPtr m_clientSpeaker; + HashMap<SpeakerId, SpeakerPtr> m_speakers; + + HashSet<SpeakerPtr> m_activeSpeakers; + + OpusEncoderPtr m_encoder; + + bool m_deviceOpen = false; + Maybe<String> m_deviceName; + VoiceInputMode m_inputMode; + VoiceChannelMode m_channelMode; + + ApplicationControllerPtr m_applicationController; +}; + +} + +#endif
\ No newline at end of file |