feat(audio): integrate OpenAL audio engine with WAV/MP3/FLAC support

This commit is contained in:
2026-07-06 15:56:42 +08:00
parent c098fdbc72
commit d23540ff86
18 changed files with 400 additions and 3 deletions

View File

@@ -32,6 +32,8 @@ add_subdirectory(third_party/glad)
add_subdirectory(third_party/imgui)
add_subdirectory(third_party/dr_libs)
add_executable(${PROJECT_NAME})
add_subdirectory(src)
@@ -129,6 +131,7 @@ target_link_libraries(${PROJECT_NAME}
Freetype::Freetype
tomlplusplus::tomlplusplus
imgui
dr_libs
tbb
protobuf::libprotobuf
absl::log

View File

@@ -1,4 +1,5 @@
#pragma once
#include "Cubed/audio/audio_engine.hpp"
#include "Cubed/gameplay/client_world.hpp"
#include "Cubed/gameplay/network_server.hpp"
#include "Cubed/gameplay/server_world.hpp"
@@ -62,6 +63,8 @@ private:
Window m_window{m_renderer};
AudioEngine m_audio;
inline static double last_time = glfwGetTime();
inline static double current_time = glfwGetTime();
inline static double delta_time = 0.0f;

View File

@@ -0,0 +1,20 @@
#pragma once
#include "Cubed/audio/audio_data.hpp"
#include <AL/al.h>
namespace Cubed {
class AudioBuffer {
public:
AudioBuffer(const AudioData& data);
AudioBuffer(const AudioBuffer&) = delete;
AudioBuffer(AudioBuffer&&) = delete;
AudioBuffer& operator=(const AudioBuffer&) = delete;
AudioBuffer& operator=(AudioBuffer&&) = delete;
~AudioBuffer();
ALuint buffer() const;
private:
ALuint m_buffer = 0;
void set_data(const AudioData& data);
};
} // namespace Cubed

View File

@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
#include <vector>
namespace Cubed {
struct AudioData {
std::vector<int16_t> pcm;
uint32_t channels;
uint32_t sample_rate;
};
} // namespace Cubed

View File

@@ -0,0 +1,38 @@
#pragma once
#include "Cubed/audio/audio_source.hpp"
#include "Cubed/audio/sound_manager.hpp"
#include <AL/al.h>
#include <AL/alc.h>
#include <glm/glm.hpp>
#include <memory>
#include <string>
#include <unordered_map>
namespace Cubed {
class ClientWorld;
class AudioEngine {
public:
AudioEngine();
AudioEngine(const AudioEngine&) = delete;
AudioEngine(AudioEngine&&) = delete;
AudioEngine& operator=(const AudioEngine&) = delete;
AudioEngine& operator=(AudioEngine&&) = delete;
~AudioEngine();
void init();
void play_bgm();
void update(glm::vec3 listener_pos);
private:
using SourcePool = std::unordered_map<std::string, ALuint>;
bool m_init{false};
ALCdevice* device{nullptr};
ALCcontext* context{nullptr};
glm::vec3 listener_pos;
std::unique_ptr<AudioSource> m_bgm;
SoundManager m_sounds;
};
} // namespace Cubed

View File

@@ -0,0 +1,16 @@
#pragma once
#include "Cubed/audio/audio_data.hpp"
#include <filesystem>
namespace Cubed {
class AudioLoader {
public:
static AudioData load(const std::filesystem::path& path);
private:
static AudioData load_wav(const std::filesystem::path& path);
static AudioData load_mp3(const std::filesystem::path& path);
static AudioData load_flac(const std::filesystem::path& path);
};
} // namespace Cubed

View File

@@ -0,0 +1,26 @@
#pragma once
#include "Cubed/audio/audio_buffer.hpp"
#include <AL/al.h>
namespace Cubed {
enum class AudioState { INITIAL, PLAYING, PAUSED, STOPPED };
class AudioSource {
public:
AudioSource();
~AudioSource();
void set_buffer(const AudioBuffer& buffer);
void set_loop(bool on = true);
void play();
void stop();
void pause();
AudioState state();
private:
ALuint m_source = 0;
};
} // namespace Cubed

View File

@@ -0,0 +1,19 @@
#pragma once
#include "Cubed/audio/audio_buffer.hpp"
#include <string>
#include <unordered_map>
namespace Cubed {
class SoundManager {
public:
SoundManager();
~SoundManager();
void load(const std::string& name);
const AudioBuffer& get_buffer(const std::string& name) const;
void init();
void clear();
private:
std::unordered_map<std::string, AudioBuffer> m_buffers;
};
} // namespace Cubed

View File

@@ -1,4 +1,5 @@
#pragma once
#include "Cubed/audio/audio_engine.hpp"
#include "Cubed/gameplay/block.hpp"
#include "Cubed/gameplay/chunk_pos.hpp"
#include "Cubed/gameplay/client_chunk.hpp"
@@ -40,7 +41,7 @@ struct PlayerRenderData {
class ClientWorld {
public:
ClientWorld();
ClientWorld(AudioEngine& auido);
~ClientWorld();
void init(std::string_view player_name,
std::shared_ptr<NetworkClient> client);
@@ -112,6 +113,7 @@ private:
ClientPlayer m_player;
OtherPlayerHashMap m_player_info;
ChunkHashMap m_chunks;
AudioEngine& m_audio;
std::vector<glm::vec4> m_planes;
std::jthread m_client_thread;

View File

@@ -44,4 +44,10 @@ target_sources(${PROJECT_NAME}
gameplay/session.cpp
gameplay/network_client.cpp
player_renderer.cpp
audio/audio_engine.cpp
audio/audio_loader.cpp
audio/audio_source.cpp
audio/audio_buffer.cpp
audio/sound_manager.cpp
audio/source_pool.cpp
)

View File

@@ -12,7 +12,7 @@
#include <imgui_impl_glfw.h>
namespace Cubed {
App::App() {}
App::App() : m_client_world(m_audio) {}
App::~App() {
if (m_client) {
@@ -59,6 +59,8 @@ void App::init(int argc, char** argv) {
cursor_enter_callback);
glfwSetCharCallback(m_window.get_glfw_window(), char_callback);
m_audio.init();
ChunkGenerator::init();
BlockManager::init();
m_renderer.init();
@@ -357,6 +359,7 @@ void App::update() {
m_renderer.update_fov(fov + 5.0f);
}
}
m_audio.update(m_camera.get_camera_pos());
m_renderer.update(delta_time);
}

View File

@@ -0,0 +1,22 @@
#include "Cubed/audio/audio_buffer.hpp"
namespace Cubed {
AudioBuffer::AudioBuffer(const AudioData& data) {
alGenBuffers(1, &m_buffer);
set_data(data);
}
AudioBuffer::~AudioBuffer() { alDeleteBuffers(1, &m_buffer); }
ALuint AudioBuffer::buffer() const { return m_buffer; }
void AudioBuffer::set_data(const AudioData& data) {
ALenum format;
if (data.channels == 1) {
format = AL_FORMAT_MONO16;
} else {
format = AL_FORMAT_STEREO16;
}
alBufferData(m_buffer, format, data.pcm.data(), data.pcm.size(),
data.sample_rate);
}
} // namespace Cubed

View File

@@ -0,0 +1,46 @@
#include "Cubed/audio/audio_engine.hpp"
#include "Cubed/tools/log.hpp"
#include <stdexcept>
namespace Cubed {
AudioEngine::AudioEngine() {};
AudioEngine::~AudioEngine() {
if (!m_init) {
return;
}
m_bgm.reset();
m_sounds.clear();
alcMakeContextCurrent(nullptr);
alcDestroyContext(context);
alcCloseDevice(device);
}
void AudioEngine::init() {
device = alcOpenDevice(NULL);
if (!device) {
throw std::runtime_error("Failed to open OpenAL device.");
}
context = alcCreateContext(device, nullptr);
if (!context) {
throw std::runtime_error("Failed to create OpenAL context.");
}
alcMakeContextCurrent(context);
m_sounds.init();
m_bgm = std::make_unique<AudioSource>();
m_bgm->set_buffer(m_sounds.get_buffer("bgm/bgm001.mp3"));
Logger::info("Audio Engine Init Success");
m_init = true;
}
void AudioEngine::play_bgm() { m_bgm->play(); }
void AudioEngine::update(glm::vec3 listener_pos) {}
} // namespace Cubed

View File

@@ -0,0 +1,90 @@
#include "Cubed/audio/audio_loader.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include <dr_flac.h>
#include <dr_mp3.h>
#include <dr_wav.h>
namespace fs = std::filesystem;
namespace Cubed {
AudioData AudioLoader::load(const std::filesystem::path& path) {
if (!fs::is_regular_file(path)) {
std::string err = std::format("Path {} is not a file", path.string());
ASSERT_MSG(false, err);
throw std::runtime_error(err);
}
auto ext = path.extension().string();
if (ext == ".wav") {
return load_wav(path);
}
if (ext == ".mp3") {
return load_mp3(path);
}
if (ext == ".flac") {
return load_flac(path);
}
throw std::runtime_error(std::format("Unsupported audio format {}", ext));
}
AudioData AudioLoader::load_wav(const std::filesystem::path& path) {
drwav wav{};
if (!drwav_init_file(&wav, path.string().c_str(), nullptr)) {
throw std::runtime_error("Failed to open wav");
}
AudioData data;
data.channels = wav.channels;
data.sample_rate = wav.sampleRate;
data.pcm.resize(wav.totalPCMFrameCount * wav.channels);
drwav_read_pcm_frames_s16(&wav, wav.totalPCMFrameCount, data.pcm.data());
drwav_uninit(&wav);
return data;
}
AudioData AudioLoader::load_mp3(const std::filesystem::path& path) {
drmp3_config config;
drmp3_uint64 frame_count;
int16_t* pcm = drmp3_open_file_and_read_pcm_frames_s16(
path.string().c_str(), &config, &frame_count, nullptr);
if (!pcm)
throw std::runtime_error("mp3 load failed");
AudioData data;
data.channels = config.channels;
data.sample_rate = config.sampleRate;
data.pcm.assign(pcm, pcm + frame_count * config.channels);
drmp3_free(pcm, nullptr);
return data;
}
AudioData AudioLoader::load_flac(const std::filesystem::path& path) {
drflac_uint64 frame_count;
unsigned int channels;
unsigned int sample_rate;
int16_t* pcm = drflac_open_file_and_read_pcm_frames_s16(
path.string().c_str(), &channels, &sample_rate, &frame_count, nullptr);
if (!pcm)
throw std::runtime_error("Failed to load flac");
AudioData data;
data.channels = channels;
data.sample_rate = sample_rate;
data.pcm.assign(pcm, pcm + frame_count * channels);
drflac_free(pcm, nullptr);
return data;
}
} // namespace Cubed

View File

@@ -0,0 +1,50 @@
#include "Cubed/audio/audio_source.hpp"
#include <stdexcept>
namespace Cubed {
AudioSource::AudioSource() { alGenSources(1, &m_source); }
AudioSource::~AudioSource() {
if (m_source != 0) {
stop();
alDeleteSources(1, &m_source);
}
}
void AudioSource::set_buffer(const AudioBuffer& buffer) {
if (state() == AudioState::PLAYING) {
stop();
}
alSourcei(m_source, AL_BUFFER, buffer.buffer());
}
void AudioSource::set_loop(bool on) {
if (on) {
alSourcei(m_source, AL_LOOPING, AL_TRUE);
} else {
alSourcei(m_source, AL_LOOPING, AL_FALSE);
}
}
void AudioSource::play() { alSourcePlay(m_source); }
void AudioSource::stop() { alSourceStop(m_source); }
void AudioSource::pause() { alSourcePause(m_source); }
AudioState AudioSource::state() {
ALint state;
alGetSourcei(m_source, AL_SOURCE_STATE, &state);
switch (state) {
case AL_INITIAL:
return AudioState::INITIAL;
case AL_PLAYING:
return AudioState::PLAYING;
case AL_STOPPED:
return AudioState::STOPPED;
case AL_PAUSED:
return AudioState::PAUSED;
}
throw std::runtime_error("Invaild state");
}
} // namespace Cubed

View File

@@ -0,0 +1,37 @@
#include "Cubed/audio/sound_manager.hpp"
#include "Cubed/audio/audio_loader.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include <filesystem>
namespace fs = std::filesystem;
namespace Cubed {
SoundManager::SoundManager() {}
SoundManager::~SoundManager() { clear(); }
void SoundManager::clear() { m_buffers.clear(); }
void SoundManager::init() { load("bgm/bgm001.mp3"); }
void SoundManager::load(const std::string& name) {
fs::path sound_path{std::string(ASSETS_PATH) + "sound/" + name};
try {
AudioData data = AudioLoader::load(sound_path);
auto [_, inserted] = m_buffers.try_emplace(name, data);
if (!inserted) {
Logger::error("Key Already exist, check the sound name {}", name);
}
} catch (const std::exception& e) {
Logger::error("Load Sound Error {}", e.what());
}
}
const AudioBuffer& SoundManager::get_buffer(const std::string& name) const {
auto it = m_buffers.find(name);
if (it == m_buffers.end()) {
std::string err = std::format("Can't Find Buffer {}", name);
ASSERT_MSG(false, err);
throw std::runtime_error(err);
}
return it->second;
}
} // namespace Cubed

View File

@@ -107,6 +107,10 @@ void DevPanel::show_about_table_bar() {
ImGui::Text("OpenAl Soft");
ImGui::Text("dr_libs");
ImGui::Separator();
ImGui::Text("Music");
ImGui::Text("'Find a Peaceful Place' by ROZKOL (Free Music Archive), "
"CC BY 4.0.");
ImGui::Separator();
ImGui::Text("Special Thanks");
ImGui::Text("TANGERIME");
ImGui::Text("SkyOnPole");

View File

@@ -20,7 +20,8 @@ struct ChunkRenderData {
};
} // namespace
ClientWorld::ClientWorld() : m_player(*this) {}
ClientWorld::ClientWorld(AudioEngine& auido)
: m_player(*this), m_audio(auido) {}
ClientWorld::~ClientWorld() {
stop_client_thread();
@@ -356,6 +357,7 @@ void ClientWorld::init(std::string_view player_name,
// request login
Logger::info("Send Login Request");
m_client->send(make_packet(req), 0);
m_audio.play_bgm();
}
void ClientWorld::start_client_thread(std::string_view uuid) {