feat(audio): implement voice chat using Opus codec

Add voice chat support with Opus encoding/decoding, push-to-talk (V key), and new audio recording infrastructure.
This commit is contained in:
2026-07-20 15:14:01 +08:00
parent 5b081c0b0b
commit bf6edeb812
24 changed files with 404 additions and 31 deletions

View File

@@ -147,6 +147,7 @@ target_link_libraries(${PROJECT_NAME}
zstd::zstd zstd::zstd
OpenAL::OpenAL OpenAL::OpenAL
harfbuzz::harfbuzz harfbuzz::harfbuzz
Opus::Opus
$<$<PLATFORM_ID:Windows>:ws2_32> $<$<PLATFORM_ID:Windows>:ws2_32>
) )

View File

@@ -9,7 +9,7 @@ find_package(OpenAL REQUIRED)
find_package(harfbuzz REQUIRED) find_package(harfbuzz REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
find_package(SDL3 REQUIRED) find_package(SDL3 REQUIRED)
find_package(Opus REQUIRED)
# Third-party libraries # Third-party libraries
FetchContent_Declare( FetchContent_Declare(

View File

@@ -0,0 +1,25 @@
include(FindPackageHandleStandardArgs)
find_path(OPUS_INCLUDE_DIR
NAMES opus/opus.h opus.h
)
find_library(OPUS_LIBRARY
NAMES opus
)
find_package_handle_standard_args(
Opus
REQUIRED_VARS
OPUS_INCLUDE_DIR
OPUS_LIBRARY
)
if(Opus_FOUND)
add_library(Opus::Opus UNKNOWN IMPORTED)
set_target_properties(Opus::Opus PROPERTIES
IMPORTED_LOCATION "${OPUS_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${OPUS_INCLUDE_DIR}"
)
endif()

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "Cubed/audio/audio_fade.hpp" #include "Cubed/audio/audio_fade.hpp"
#include "Cubed/audio/audio_recording.hpp"
#include "Cubed/audio/audio_source.hpp" #include "Cubed/audio/audio_source.hpp"
#include "Cubed/audio/sound_manager.hpp" #include "Cubed/audio/sound_manager.hpp"
#include "Cubed/audio/source_pool.hpp" #include "Cubed/audio/source_pool.hpp"
@@ -10,13 +11,15 @@
#include <AL/alc.h> #include <AL/alc.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <memory> #include <memory>
#include <opus/opus.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
namespace Cubed { namespace Cubed {
class ClientWorld; class NetworkClient;
class AudioEngine { class AudioEngine {
public: public:
AudioEngine(Config& config); AudioEngine(Config& config);
AudioEngine(const AudioEngine&) = delete; AudioEngine(const AudioEngine&) = delete;
@@ -41,11 +44,22 @@ public:
float& bgm_target_volume(); float& bgm_target_volume();
void set_client(std::weak_ptr<NetworkClient> client);
void send_voice(const std::array<int16_t, AudioRecording::FRAME_SAMPLES>&);
void receive_voice(std::span<char> opus, const glm::vec3& pos);
AudioRecording& audio_recording();
private: private:
using FadeMap = std::unordered_map<std::string, AudioFade>; using FadeMap = std::unordered_map<std::string, AudioFade>;
bool m_init{false}; bool m_init{false};
ALCdevice* device{nullptr}; ALCdevice* device{nullptr};
ALCcontext* context{nullptr}; ALCcontext* context{nullptr};
OpusEncoder* m_encoder{nullptr};
OpusDecoder* m_decoder{nullptr};
AudioRecording m_recording;
std::weak_ptr<NetworkClient> m_client;
glm::vec3 listener_pos; glm::vec3 listener_pos;
std::unique_ptr<AudioSource> m_bgm; std::unique_ptr<AudioSource> m_bgm;
FadeMap m_fade_map; FadeMap m_fade_map;

View File

@@ -0,0 +1,33 @@
#pragma once
#include <alc.h>
#include <array>
#include <cstdint>
namespace Cubed {
class AudioEngine;
class AudioRecording {
public:
static constexpr int SAMPLE_RATE = 48000;
static constexpr int FRAME_MS = 20;
static constexpr int FRAME_SAMPLES = SAMPLE_RATE * FRAME_MS / 1000;
AudioRecording(AudioEngine& engine);
AudioRecording(const AudioRecording&) = delete;
AudioRecording(AudioRecording&&) = delete;
AudioRecording& operator=(const AudioRecording&) = delete;
AudioRecording& operator=(AudioRecording&&) = delete;
~AudioRecording();
void update();
void init();
void start();
void stop();
bool is_recording() const;
private:
AudioEngine& m_engine;
ALCdevice* m_capture = nullptr;
bool m_recording = false;
void send_voice(const std::array<int16_t, FRAME_SAMPLES>& pcm);
};
} // namespace Cubed

View File

@@ -5,6 +5,7 @@
#include <AL/al.h> #include <AL/al.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <memory>
namespace Cubed { namespace Cubed {
enum class AudioState { INITIAL, PLAYING, PAUSED, STOPPED }; enum class AudioState { INITIAL, PLAYING, PAUSED, STOPPED };
@@ -21,8 +22,9 @@ public:
void set_pitch(float pitch); void set_pitch(float pitch);
void play(); void play();
void play_2d(const AudioBuffer& buffer); void play_2d(const AudioBuffer& buffer);
void play_2d(std::unique_ptr<AudioBuffer> buffer);
void play_3d(const AudioBuffer& buffer, const glm::vec3& pos); void play_3d(const AudioBuffer& buffer, const glm::vec3& pos);
void play_3d(std::unique_ptr<AudioBuffer> buffer, const glm::vec3& pos);
void stop(); void stop();
void pause(); void pause();
float duration() const; float duration() const;
@@ -42,6 +44,8 @@ public:
void clear_effect_slot(); void clear_effect_slot();
private: private:
std::unique_ptr<AudioBuffer> m_buffer;
ALuint m_source = 0; ALuint m_source = 0;
float m_target_volume = 1.0f; float m_target_volume = 1.0f;
float m_duration = 0.0f; float m_duration = 0.0f;

View File

@@ -4,7 +4,7 @@
namespace Cubed { namespace Cubed {
class SourcePool { class SourcePool {
private: private:
std::vector<AudioSource> m_sources; std::vector<std::unique_ptr<AudioSource>> m_sources;
public: public:
explicit SourcePool(size_t size); explicit SourcePool(size_t size);
@@ -15,9 +15,10 @@ public:
SourcePool& operator=(SourcePool&&) = delete; SourcePool& operator=(SourcePool&&) = delete;
void update(); void update();
[[nodiscard]]
AudioSource* acquire(); AudioSource* acquire();
std::vector<AudioSource>& sources(); std::vector<std::unique_ptr<AudioSource>>& sources();
}; };
} // namespace Cubed } // namespace Cubed

View File

@@ -106,7 +106,7 @@ public:
void receive_chat_message(ChatMsg& msg); void receive_chat_message(ChatMsg& msg);
void send_chat_message(ChatMessage& message); void send_chat_message(ChatMessage& message);
void receive_voice_message(VoiceMsg& msg);
template <typename Fn> template <typename Fn>
void register_ticktimer(std::string_view id, TickType threshold, Fn&& f) { void register_ticktimer(std::string_view id, TickType threshold, Fn&& f) {
m_ticktimers.emplace( m_ticktimers.emplace(
@@ -115,6 +115,11 @@ public:
} }
private: private:
struct VoiceMessage {
std::string data;
glm::vec3 pos;
};
std::atomic<bool> m_is_pending_delete_queue_free{false}; std::atomic<bool> m_is_pending_delete_queue_free{false};
std::mutex m_delete_vbo_mutex; std::mutex m_delete_vbo_mutex;
std::mutex m_delete_vao_mutex; std::mutex m_delete_vao_mutex;
@@ -153,6 +158,7 @@ private:
tbb::concurrent_queue<ChunkPos> m_dirty_chunk_queue; tbb::concurrent_queue<ChunkPos> m_dirty_chunk_queue;
tbb::concurrent_queue<PendingSound> m_pending_sound; tbb::concurrent_queue<PendingSound> m_pending_sound;
tbb::concurrent_queue<ChatMessage> m_message_queue; tbb::concurrent_queue<ChatMessage> m_message_queue;
tbb::concurrent_queue<VoiceMessage> m_voice_queue;
std::deque<ChunkPos> m_dirty_queue; std::deque<ChunkPos> m_dirty_queue;
std::vector<const ChunkRenderSnapshot*> m_render_snapshots; std::vector<const ChunkRenderSnapshot*> m_render_snapshots;

View File

@@ -21,6 +21,7 @@ public:
bool is_connect_error() const; bool is_connect_error() const;
std::string get_error_string() const; std::string get_error_string() const;
void clear_error(); void clear_error();
ClientWorld& world();
private: private:
struct Task { struct Task {

View File

@@ -62,6 +62,7 @@ enum class PacketEnum : uint16_t {
S2C_CLEAR_ALL_CHUNKS = 3005, S2C_CLEAR_ALL_CHUNKS = 3005,
UPDATE_TIME = 3006, UPDATE_TIME = 3006,
CHAT_MSG = 4001, CHAT_MSG = 4001,
VOICE_MSG = 4002,
PING = 9001, PING = 9001,
PONG = 9002 PONG = 9002
@@ -125,6 +126,9 @@ template <> constexpr uint16_t get_packet_id<PlayerWaterSound>() {
template <> constexpr uint16_t get_packet_id<ChatMsg>() { template <> constexpr uint16_t get_packet_id<ChatMsg>() {
return std::to_underlying(PacketEnum::CHAT_MSG); return std::to_underlying(PacketEnum::CHAT_MSG);
} }
template <> constexpr uint16_t get_packet_id<VoiceMsg>() {
return std::to_underlying(PacketEnum::VOICE_MSG);
}
template <typename T> template <typename T>
requires std::derived_from<T, google::protobuf::Message> requires std::derived_from<T, google::protobuf::Message>

View File

@@ -81,7 +81,7 @@ public:
void handle_block_change(const BlockChangeReq& req); void handle_block_change(const BlockChangeReq& req);
void handle_chat_message(ChatMsg& msg); void handle_chat_message(ChatMsg& msg);
void handle_voice_message(VoiceMsg& msg);
int chunk_size() const; int chunk_size() const;
template <typename Fn> template <typename Fn>
void register_timer(std::string_view id, TickType threshold, Fn&& f) { void register_timer(std::string_view id, TickType threshold, Fn&& f) {

View File

@@ -86,4 +86,5 @@ target_sources(${PROJECT_NAME}
localization.cpp localization.cpp
tools/system_locate.cpp tools/system_locate.cpp
ui/chat_box.cpp ui/chat_box.cpp
audio/audio_recording.cpp
) )

View File

@@ -1,13 +1,19 @@
#include "Cubed/audio/audio_engine.hpp" #include "Cubed/audio/audio_engine.hpp"
#include "Cubed/audio/audio_error.hpp" #include "Cubed/audio/audio_error.hpp"
#include "Cubed/gameplay/client_world.hpp"
#include "Cubed/gameplay/network_client.hpp"
#include "Cubed/tools/cubed_assert.hpp" #include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp" #include "Cubed/tools/log.hpp"
#include <stdexcept> #include <stdexcept>
using namespace google::protobuf;
namespace {
constexpr std::size_t OPUS_MAX_PACKET_SIZE = 400;
}
namespace Cubed { namespace Cubed {
AudioEngine::AudioEngine(Config& config) : m_config(config) {}; AudioEngine::AudioEngine(Config& config)
: m_recording(*this), m_config(config) {};
AudioEngine::~AudioEngine() { AudioEngine::~AudioEngine() {
if (!m_init) { if (!m_init) {
@@ -21,13 +27,35 @@ AudioEngine::~AudioEngine() {
m_low_pass_filter.reset(); m_low_pass_filter.reset();
m_underwater_effect.reset(); m_underwater_effect.reset();
m_underwater_slot.reset(); m_underwater_slot.reset();
opus_encoder_destroy(m_encoder);
opus_decoder_destroy(m_decoder);
alcMakeContextCurrent(nullptr); alcMakeContextCurrent(nullptr);
alcDestroyContext(context); alcDestroyContext(context);
alcCloseDevice(device); alcCloseDevice(device);
} }
void AudioEngine::init() { void AudioEngine::init() {
{
int error;
m_encoder = opus_encoder_create(AudioRecording::SAMPLE_RATE,
1, // Mono
OPUS_APPLICATION_VOIP, &error);
if (error != OPUS_OK) {
Logger::error("Can't Create Opus Encoder Error {}", error);
}
opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(24000));
opus_encoder_ctl(m_encoder, OPUS_SET_COMPLEXITY(5));
opus_encoder_ctl(m_encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
}
{
int error;
m_decoder = opus_decoder_create(AudioRecording::SAMPLE_RATE, 1, &error);
if (error != OPUS_OK) {
Logger::error("Can't Create Opus Encoder Error {}", error);
}
}
device = alcOpenDevice(NULL); device = alcOpenDevice(NULL);
if (!device) { if (!device) {
throw std::runtime_error("Failed to open OpenAL device."); throw std::runtime_error("Failed to open OpenAL device.");
@@ -62,7 +90,7 @@ void AudioEngine::init() {
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
check_al_error(); check_al_error();
m_recording.init();
m_music_volume = m_config.get("volume.music", 1.0f); m_music_volume = m_config.get("volume.music", 1.0f);
m_sfx_volume = m_config.get("volume.SFX", 1.0f); m_sfx_volume = m_config.get("volume.SFX", 1.0f);
@@ -144,10 +172,10 @@ void AudioEngine::play_2d(const std::string& sound, bool check) {
return; return;
} }
auto* source = m_pool->acquire(); auto* source = m_pool->acquire();
source->set_volume(m_sfx_volume);
if (!source) { if (!source) {
Logger::error("Source is Full"); Logger::error("Source is Full");
} }
source->set_volume(m_sfx_volume);
try { try {
auto& buffer = m_sounds.get_buffer(sound); auto& buffer = m_sounds.get_buffer(sound);
@@ -181,6 +209,7 @@ void AudioEngine::update() {
fade.update(); fade.update();
} }
m_pool->update(); m_pool->update();
m_recording.update();
} }
void AudioEngine::reload_config() { void AudioEngine::reload_config() {
@@ -201,11 +230,11 @@ void AudioEngine::underwater_change(bool underwater) {
} }
for (auto& source : m_pool->sources()) { for (auto& source : m_pool->sources()) {
if (m_underwater) { if (m_underwater) {
source.set_filter(*m_low_pass_filter); source->set_filter(*m_low_pass_filter);
source.set_effect_slot(*m_underwater_slot); source->set_effect_slot(*m_underwater_slot);
} else { } else {
source.clear_filter(); source->clear_filter();
source.clear_effect_slot(); source->clear_effect_slot();
} }
} }
if (underwater) { if (underwater) {
@@ -219,4 +248,87 @@ void AudioEngine::underwater_change(bool underwater) {
float& AudioEngine::bgm_target_volume() { return m_bgm->target_volume(); } float& AudioEngine::bgm_target_volume() { return m_bgm->target_volume(); }
void AudioEngine::set_client(std::weak_ptr<NetworkClient> client) {
m_client = client;
}
void AudioEngine::send_voice(
const std::array<int16_t, AudioRecording::FRAME_SAMPLES>& pcm) {
{
AudioData data;
data.pcm = {pcm.begin(), pcm.end()};
data.channels = 1;
data.sample_rate = AudioRecording::SAMPLE_RATE;
auto* source = m_pool->acquire();
source->set_volume(m_sfx_volume);
if (!source) {
Logger::error("Source is Full");
return;
}
std::unique_ptr<AudioBuffer> buffer =
std::make_unique<AudioBuffer>(data);
if (m_efx_supported && m_underwater) {
source->set_filter(*m_low_pass_filter);
source->set_effect_slot(*m_underwater_slot);
}
source->play_2d(std::move(buffer));
}
/*
std::array<uint8_t, OPUS_MAX_PACKET_SIZE> opus;
int len = opus_encode(m_encoder, pcm.data(), AudioRecording::FRAME_SAMPLES,
opus.data(), opus.size());
if (len < 0) {
Logger::error("Opus encode failed: {}", opus_strerror(len));
return;
}
Logger::info("opus encode len={}", len);
if (auto c = m_client.lock()) {
Arena arena;
auto msg = Arena::Create<VoiceMsg>(&arena);
msg->set_uuid(c->world().get_player().get_uuid());
msg->set_opus_data(reinterpret_cast<char*>(opus.data()), len);
auto* pos = msg->mutable_pos();
auto p = c->world().get_player().get_player_pos();
pos->set_x(p.x);
pos->set_y(p.y);
pos->set_z(p.z);
c->send(make_packet(*msg));
}
*/
}
void AudioEngine::receive_voice(std::span<char> opus, const glm::vec3& pos) {
AudioData data;
data.pcm.resize(AudioRecording::FRAME_SAMPLES);
data.channels = 1;
data.sample_rate = AudioRecording::SAMPLE_RATE;
int len = opus_decode(
m_decoder, reinterpret_cast<const uint8_t*>(opus.data()), opus.size(),
data.pcm.data(), AudioRecording::FRAME_SAMPLES, 0);
if (len < 0) {
Logger::error("Opus decode failed: {}", opus_strerror(len));
return;
}
Logger::info("decode samples={}", len);
Logger::info("Receive Vocie and start play");
auto* source = m_pool->acquire();
source->set_volume(m_sfx_volume);
if (!source) {
Logger::error("Source is Full");
return;
}
for (int i = 0; i < 10; ++i)
Logger::info("recv {}", data.pcm[i]);
std::unique_ptr<AudioBuffer> buffer = std::make_unique<AudioBuffer>(data);
if (m_efx_supported && m_underwater) {
source->set_filter(*m_low_pass_filter);
source->set_effect_slot(*m_underwater_slot);
}
source->play_3d(std::move(buffer), pos);
}
AudioRecording& AudioEngine::audio_recording() { return m_recording; }
} // namespace Cubed } // namespace Cubed

View File

@@ -0,0 +1,74 @@
#include "Cubed/audio/audio_recording.hpp"
#include "Cubed/audio/audio_engine.hpp"
#include "Cubed/tools/log.hpp"
#include <al.h>
namespace Cubed {
AudioRecording::AudioRecording(AudioEngine& engine)
: m_engine(engine) {
};
AudioRecording::~AudioRecording() {
if (m_capture) {
if (m_recording) {
stop();
}
alcCaptureCloseDevice(m_capture);
}
}
void AudioRecording::init() {
m_capture =
alcCaptureOpenDevice(nullptr, SAMPLE_RATE, AL_FORMAT_MONO16, 4096);
if (m_capture) {
Logger::info("Open Audio Capture Success");
} else {
Logger::error("Fail to Open Audio Capture Device");
}
ALCint freq = 0;
alcGetIntegerv(m_capture, ALC_FREQUENCY, 1, &freq);
Logger::info("Capture frequency={}", freq);
}
void AudioRecording::start() {
ALCint samples;
alcGetIntegerv(m_capture, ALC_CAPTURE_SAMPLES, 1, &samples);
std::vector<int16_t> dump(samples);
alcCaptureSamples(m_capture, dump.data(), samples);
alcCaptureStart(m_capture);
m_recording = true;
}
void AudioRecording::stop() {
alcCaptureStop(m_capture);
m_recording = false;
}
bool AudioRecording::is_recording() const { return m_recording; }
void AudioRecording::update() {
if (!m_recording || !m_capture) {
return;
}
ALCint available = 0;
alcGetIntegerv(m_capture, ALC_CAPTURE_SAMPLES, 1, &available);
while (available >= FRAME_SAMPLES) {
std::array<int16_t, FRAME_SAMPLES> pcm;
alcCaptureSamples(m_capture, pcm.data(), FRAME_SAMPLES);
for (int i = 0; i < 20; i++) {
Logger::info("pcm[{}]={}", i, pcm[i]);
}
send_voice(pcm);
available -= FRAME_SAMPLES;
}
}
void AudioRecording::send_voice(const std::array<int16_t, FRAME_SAMPLES>& pcm) {
m_engine.send_voice(pcm);
}
} // namespace Cubed

View File

@@ -91,12 +91,19 @@ void AudioSource::play_2d(const AudioBuffer& buffer) {
set_buffer_2d(buffer); set_buffer_2d(buffer);
play(); play();
} }
void AudioSource::play_2d(std::unique_ptr<AudioBuffer> buffer) {
m_buffer = std::move(buffer);
play_2d(*m_buffer);
}
void AudioSource::play_3d(const AudioBuffer& buffer, const glm::vec3& pos) { void AudioSource::play_3d(const AudioBuffer& buffer, const glm::vec3& pos) {
set_buffer_3d(buffer, pos); set_buffer_3d(buffer, pos);
play(); play();
} }
void AudioSource::play_3d(std::unique_ptr<AudioBuffer> buffer,
const glm::vec3& pos) {
m_buffer = std::move(buffer);
play_3d(*m_buffer, pos);
}
void AudioSource::stop() { alSourceStop(m_source); } void AudioSource::stop() { alSourceStop(m_source); }
void AudioSource::pause() { alSourcePause(m_source); } void AudioSource::pause() { alSourcePause(m_source); }
@@ -163,7 +170,7 @@ void AudioSource::reset() {
clear_filter(); clear_filter();
clear_effect_slot(); clear_effect_slot();
m_buffer.reset();
m_duration = 0.0f; m_duration = 0.0f;
m_target_volume = 1.0f; m_target_volume = 1.0f;
m_using = false; m_using = false;

View File

@@ -1,24 +1,32 @@
#include "Cubed/audio/source_pool.hpp" #include "Cubed/audio/source_pool.hpp"
namespace Cubed { namespace Cubed {
SourcePool::SourcePool(size_t size) { m_sources.resize(size); } SourcePool::SourcePool(size_t size) {
m_sources.reserve(size);
for (size_t i = 0; i < size; ++i) {
m_sources.emplace_back(std::make_unique<AudioSource>());
}
}
SourcePool::~SourcePool() { m_sources.clear(); } SourcePool::~SourcePool() { m_sources.clear(); }
void SourcePool::update() { void SourcePool::update() {
for (auto& source : m_sources) { for (auto& source_ptr : m_sources) {
if (source.in_use() && source.state() == AudioState::STOPPED) { if (source_ptr->in_use() &&
source.reset(); source_ptr->state() == AudioState::STOPPED) {
source_ptr->reset();
} }
} }
} }
AudioSource* SourcePool::acquire() { AudioSource* SourcePool::acquire() {
for (auto& source : m_sources) { for (auto& source_ptr : m_sources) {
if (!source.in_use()) { if (!source_ptr->in_use()) {
source.mark_in_use(); source_ptr->mark_in_use();
return &source; return source_ptr.get();
} }
} }
return nullptr; return nullptr;
} }
std::vector<AudioSource>& SourcePool::sources() { return m_sources; } std::vector<std::unique_ptr<AudioSource>>& SourcePool::sources() {
return m_sources;
}
} // namespace Cubed } // namespace Cubed

View File

@@ -754,6 +754,13 @@ void ClientWorld::request_exit() {
void ClientWorld::receive_chat_message(ChatMsg& msg) { void ClientWorld::receive_chat_message(ChatMsg& msg) {
m_message_queue.emplace(msg.name(), msg.msg(), Tools::get_time_ticks()); m_message_queue.emplace(msg.name(), msg.msg(), Tools::get_time_ticks());
} }
void ClientWorld::receive_voice_message(VoiceMsg& msg) {
glm::vec3 pos{msg.pos().x(), msg.pos().y(), msg.pos().z()};
Logger::info("Receive Voice From net");
m_voice_queue.emplace(msg.opus_data(), pos);
}
void ClientWorld::send_chat_message(ChatMessage& message) { void ClientWorld::send_chat_message(ChatMessage& message) {
Arena arena; Arena arena;
auto msg = Arena::Create<ChatMsg>(&arena); auto msg = Arena::Create<ChatMsg>(&arena);
@@ -932,6 +939,11 @@ void ClientWorld::update(float delta_time) {
while (m_message_queue.try_pop(message)) { while (m_message_queue.try_pop(message)) {
m_world_scene.handle_chat_message(message); m_world_scene.handle_chat_message(message);
} }
VoiceMessage vm;
while (m_voice_queue.try_pop(vm)) {
m_audio.receive_voice(vm.data, vm.pos);
}
} }
bool ClientWorld::handle_event(const Event& e) { bool ClientWorld::handle_event(const Event& e) {

View File

@@ -141,6 +141,12 @@ asio::awaitable<void> NetworkClient::read_loop() {
m_world.receive_chat_message(*msg); m_world.receive_chat_message(*msg);
} }
} break; } break;
case std::to_underlying(PacketEnum::VOICE_MSG): {
auto* msg = Arena::Create<VoiceMsg>(&arena);
if (decode_packet(*msg, body_data, header)) {
m_world.receive_voice_message(*msg);
}
}
} }
} }
} catch (const asio::system_error& e) { } catch (const asio::system_error& e) {
@@ -228,5 +234,5 @@ void NetworkClient::set_error(std::string_view error) {
m_error_string = error; m_error_string = error;
m_connect_error = true; m_connect_error = true;
} }
ClientWorld& NetworkClient::world() { return m_world; }
} // namespace Cubed } // namespace Cubed

View File

@@ -763,6 +763,44 @@ void ServerWorld::handle_chat_message(ChatMsg& msg) {
}); });
} }
void ServerWorld::handle_voice_message(VoiceMsg& msg) {
Logger::info("Get voice Message");
auto pool = m_net_thread_pool.load();
std::string uuid = msg.uuid();
std::string data = msg.opus_data();
auto pos = msg.pos();
glm::vec3 p{pos.x(), pos.y(), pos.z()};
pool->enqueue([this, uuid = std::move(uuid), data = std::move(data), p]() {
std::vector<std::shared_ptr<Session>> session;
{
std::shared_lock lock(m_player_mutex);
for (auto& [key, player] : m_players) {
if (key == uuid) {
continue;
}
session.emplace_back(player.get_session());
}
}
Arena arena;
auto msg = Arena::Create<VoiceMsg>(&arena);
msg->set_uuid(uuid);
msg->set_opus_data(data);
auto pos = msg->mutable_pos();
pos->set_x(p.x);
pos->set_y(p.y);
pos->set_z(p.z);
for (auto& s : session) {
s->send(make_packet(*msg), 5);
}
Logger::info("Send voice message sum {}", session.size());
});
}
void ServerWorld::handle_block_change(const BlockChangeReq& req) { void ServerWorld::handle_block_change(const BlockChangeReq& req) {
float x = std::floor(req.pos().x()); float x = std::floor(req.pos().x());
float y = std::floor(req.pos().y()); float y = std::floor(req.pos().y());

View File

@@ -105,6 +105,12 @@ asio::awaitable<void> Session::read_loop() {
m_server_world.handle_chat_message(*msg); m_server_world.handle_chat_message(*msg);
} }
} }
if (cmd_id == std::to_underlying(PacketEnum::VOICE_MSG)) {
auto* msg = Arena::Create<VoiceMsg>(&arena);
if (decode_packet(*msg, body_data, header)) {
m_server_world.handle_voice_message(*msg);
}
}
} }
} catch (const asio::system_error& e) { } catch (const asio::system_error& e) {
auto ec = e.code(); auto ec = e.code();

View File

@@ -1,6 +1,12 @@
syntax = "proto3"; syntax = "proto3";
import "common/vector3.proto";
message ChatMsg { message ChatMsg {
string name = 1; string name = 1;
string msg = 2; string msg = 2;
} }
message VoiceMsg {
string uuid = 1;
Vec3 pos = 2;
bytes opus_data = 3;
}

View File

@@ -141,7 +141,7 @@ void WorldScene::on_enter() {
m_dev_panel.init(); m_dev_panel.init();
m_pasue_menu.init(); m_pasue_menu.init();
m_hud_ui.init(); m_hud_ui.init();
m_scene_manager.app().audio().set_client(m_client);
m_scene_manager.app().window().set_game_running(true); m_scene_manager.app().window().set_game_running(true);
} catch (const std::exception& e) { } catch (const std::exception& e) {
m_error_ui.set_error(e.what()); m_error_ui.set_error(e.what());
@@ -290,6 +290,18 @@ bool WorldScene::handle_key_event(const KeyEvent& e) {
} }
} }
if (e.key == Key::V) {
auto& recording = m_client_world.get_audio().audio_recording();
if (e.action == KeyAction::PRESS) {
recording.start();
return true;
}
if (e.action == KeyAction::RELEASE) {
recording.stop();
return true;
}
}
if (m_paused) { if (m_paused) {
if (m_pasue_menu.handle_event(e)) { if (m_pasue_menu.handle_event(e)) {
return true; return true;

View File

@@ -60,6 +60,7 @@ void CreditsUI::init() {
add_text("protobuf"); add_text("protobuf");
add_text("zstd"); add_text("zstd");
add_text("OpenAl Soft"); add_text("OpenAl Soft");
add_text("Opus");
add_text("dr_libs"); add_text("dr_libs");
add_text("nlohmann/json"); add_text("nlohmann/json");
add_text("HarfBuzz"); add_text("HarfBuzz");

View File

@@ -5,6 +5,7 @@
"openal-soft", "openal-soft",
"harfbuzz", "harfbuzz",
"freetype", "freetype",
"opus",
{ {
"name": "sdl3", "name": "sdl3",
"default-features": false "default-features": false