mirror of
https://github.com/zhenyan121/Cubed.git
synced 2026-08-09 02:07:04 +08:00
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:
@@ -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>
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
25
cmake/modules/FindOpus.cmake
Normal file
25
cmake/modules/FindOpus.cmake
Normal 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()
|
||||||
@@ -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;
|
||||||
|
|||||||
33
include/Cubed/audio/audio_recording.hpp
Normal file
33
include/Cubed/audio/audio_recording.hpp
Normal 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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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
|
||||||
74
src/audio/audio_recording.cpp
Normal file
74
src/audio/audio_recording.cpp
Normal 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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"openal-soft",
|
"openal-soft",
|
||||||
"harfbuzz",
|
"harfbuzz",
|
||||||
"freetype",
|
"freetype",
|
||||||
|
"opus",
|
||||||
{
|
{
|
||||||
"name": "sdl3",
|
"name": "sdl3",
|
||||||
"default-features": false
|
"default-features": false
|
||||||
|
|||||||
Reference in New Issue
Block a user