feat(audio): 添加音频系统支持背景音乐和音效播放
实现完整的音频系统,包括: 1. 添加 SDL2_mixer 依赖 2. 创建音频系统核心类 AudioSystem 3. 实现音乐(Music)和音效(Sound)类 4. 在游戏主循环中初始化音频并播放背景音乐 5. 更新构建脚本以支持音频库
This commit is contained in:
63
Frostbite2D/include/frostbite2D/audio/audio_system.h
Normal file
63
Frostbite2D/include/frostbite2D/audio/audio_system.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/types/type_alias.h>
|
||||
#include <string>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/**
|
||||
* @brief 音频系统配置
|
||||
*/
|
||||
struct AudioConfig {
|
||||
int frequency = 44100;
|
||||
uint16_t format = 0x8010;
|
||||
int channels = 2;
|
||||
int chunksize = 2048;
|
||||
int maxSoundChannels = 32;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 音频系统(单例)
|
||||
*
|
||||
* 负责初始化和管理整个音频系统,包括音量控制和声道管理。
|
||||
*/
|
||||
class AudioSystem {
|
||||
public:
|
||||
static AudioSystem& get();
|
||||
|
||||
AudioSystem(const AudioSystem&) = delete;
|
||||
AudioSystem& operator=(const AudioSystem&) = delete;
|
||||
|
||||
bool init(const AudioConfig& config = AudioConfig());
|
||||
void shutdown();
|
||||
bool isInitialized() const { return initialized_; }
|
||||
|
||||
void setMasterVolume(float volume);
|
||||
float getMasterVolume() const;
|
||||
|
||||
void setSoundVolume(float volume);
|
||||
float getSoundVolume() const;
|
||||
|
||||
void setMusicVolume(float volume);
|
||||
float getMusicVolume() const;
|
||||
|
||||
void pauseAllSounds();
|
||||
void resumeAllSounds();
|
||||
void stopAllSounds();
|
||||
|
||||
int getMaxChannels() const { return maxChannels_; }
|
||||
|
||||
private:
|
||||
AudioSystem() = default;
|
||||
~AudioSystem();
|
||||
|
||||
void updateVolumes();
|
||||
|
||||
bool initialized_ = false;
|
||||
float masterVolume_ = 1.0f;
|
||||
float soundVolume_ = 1.0f;
|
||||
float musicVolume_ = 1.0f;
|
||||
int maxChannels_ = 32;
|
||||
};
|
||||
|
||||
}
|
||||
46
Frostbite2D/include/frostbite2D/audio/music.h
Normal file
46
Frostbite2D/include/frostbite2D/audio/music.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/base/RefObject.h>
|
||||
#include <frostbite2D/types/type_alias.h>
|
||||
#include <string>
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/**
|
||||
* @brief 音乐类
|
||||
*
|
||||
* 用于加载和播放长音频文件(背景音乐),支持从文件或内存加载。
|
||||
* 支持 WAV、OGG、MP3 等格式。
|
||||
*/
|
||||
class Music : public RefObject {
|
||||
public:
|
||||
static Ptr<Music> loadFromFile(const std::string& path);
|
||||
static Ptr<Music> loadFromMemory(const uint8* data, size_t size);
|
||||
|
||||
~Music();
|
||||
|
||||
bool play(int loops = -1);
|
||||
bool fadeIn(int ms, int loops = -1);
|
||||
void pause();
|
||||
void resume();
|
||||
void stop();
|
||||
bool fadeOut(int ms);
|
||||
|
||||
void setVolume(float volume);
|
||||
float getVolume() const;
|
||||
|
||||
bool isPlaying() const;
|
||||
bool isPaused() const;
|
||||
|
||||
const std::string& getPath() const { return path_; }
|
||||
|
||||
private:
|
||||
Music(Mix_Music* music, const std::string& path = "");
|
||||
|
||||
Mix_Music* music_ = nullptr;
|
||||
std::string path_;
|
||||
float volume_ = 1.0f;
|
||||
};
|
||||
|
||||
}
|
||||
37
Frostbite2D/include/frostbite2D/audio/sound.h
Normal file
37
Frostbite2D/include/frostbite2D/audio/sound.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/base/RefObject.h>
|
||||
#include <frostbite2D/types/type_alias.h>
|
||||
#include <string>
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/**
|
||||
* @brief 音效类
|
||||
*
|
||||
* 用于加载和播放短音频文件,支持从文件或内存加载。
|
||||
* 支持 WAV、OGG 等格式。
|
||||
*/
|
||||
class Sound : public RefObject {
|
||||
public:
|
||||
static Ptr<Sound> loadFromFile(const std::string& path);
|
||||
static Ptr<Sound> loadFromMemory(const uint8* data, size_t size);
|
||||
|
||||
~Sound();
|
||||
|
||||
int play(int loops = 0, int channel = -1);
|
||||
void setVolume(float volume);
|
||||
float getVolume() const;
|
||||
|
||||
const std::string& getPath() const { return path_; }
|
||||
|
||||
private:
|
||||
Sound(Mix_Chunk* chunk, const std::string& path = "");
|
||||
|
||||
Mix_Chunk* chunk_ = nullptr;
|
||||
std::string path_;
|
||||
float volume_ = 1.0f;
|
||||
};
|
||||
|
||||
}
|
||||
114
Frostbite2D/src/frostbite2D/audio/audio_system.cpp
Normal file
114
Frostbite2D/src/frostbite2D/audio/audio_system.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include <frostbite2D/audio/audio_system.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
AudioSystem& AudioSystem::get() {
|
||||
static AudioSystem instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
AudioSystem::~AudioSystem() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool AudioSystem::init(const AudioConfig& config) {
|
||||
if (initialized_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize SDL audio: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Mix_OpenAudio(config.frequency, config.format, config.channels, config.chunksize) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to open audio: %s", Mix_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return false;
|
||||
}
|
||||
|
||||
maxChannels_ = config.maxSoundChannels;
|
||||
Mix_AllocateChannels(maxChannels_);
|
||||
|
||||
initialized_ = true;
|
||||
updateVolumes();
|
||||
|
||||
SDL_Log("Audio system initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioSystem::shutdown() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mix_HaltMusic();
|
||||
Mix_HaltChannel(-1);
|
||||
Mix_CloseAudio();
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
|
||||
initialized_ = false;
|
||||
SDL_Log("Audio system shutdown");
|
||||
}
|
||||
|
||||
void AudioSystem::setMasterVolume(float volume) {
|
||||
masterVolume_ = std::clamp(volume, 0.0f, 1.0f);
|
||||
updateVolumes();
|
||||
}
|
||||
|
||||
float AudioSystem::getMasterVolume() const {
|
||||
return masterVolume_;
|
||||
}
|
||||
|
||||
void AudioSystem::setSoundVolume(float volume) {
|
||||
soundVolume_ = std::clamp(volume, 0.0f, 1.0f);
|
||||
updateVolumes();
|
||||
}
|
||||
|
||||
float AudioSystem::getSoundVolume() const {
|
||||
return soundVolume_;
|
||||
}
|
||||
|
||||
void AudioSystem::setMusicVolume(float volume) {
|
||||
musicVolume_ = std::clamp(volume, 0.0f, 1.0f);
|
||||
updateVolumes();
|
||||
}
|
||||
|
||||
float AudioSystem::getMusicVolume() const {
|
||||
return musicVolume_;
|
||||
}
|
||||
|
||||
void AudioSystem::updateVolumes() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
int musicVol = static_cast<int>(musicVolume_ * masterVolume_ * MIX_MAX_VOLUME);
|
||||
Mix_VolumeMusic(musicVol);
|
||||
}
|
||||
|
||||
void AudioSystem::pauseAllSounds() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
Mix_Pause(-1);
|
||||
}
|
||||
|
||||
void AudioSystem::resumeAllSounds() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
Mix_Resume(-1);
|
||||
}
|
||||
|
||||
void AudioSystem::stopAllSounds() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
Mix_HaltChannel(-1);
|
||||
}
|
||||
|
||||
}
|
||||
133
Frostbite2D/src/frostbite2D/audio/music.cpp
Normal file
133
Frostbite2D/src/frostbite2D/audio/music.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include <frostbite2D/audio/music.h>
|
||||
#include <frostbite2D/audio/audio_system.h>
|
||||
#include <frostbite2D/utils/asset.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
Ptr<Music> Music::loadFromFile(const std::string& path) {
|
||||
auto& asset = Asset::get();
|
||||
auto data = asset.readFileToBytes(path);
|
||||
if (!data) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read music file: %s", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto music = loadFromMemory(data->data(), data->size());
|
||||
if (music) {
|
||||
music->path_ = path;
|
||||
}
|
||||
return music;
|
||||
}
|
||||
|
||||
Ptr<Music> Music::loadFromMemory(const uint8* data, size_t size) {
|
||||
SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast<int>(size));
|
||||
if (!rw) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops from memory: %s", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Mix_Music* music = Mix_LoadMUS_RW(rw, 1);
|
||||
if (!music) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load music from memory: %s", Mix_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Ptr<Music>(new Music(music));
|
||||
}
|
||||
|
||||
Music::Music(Mix_Music* music, const std::string& path)
|
||||
: music_(music), path_(path) {
|
||||
}
|
||||
|
||||
Music::~Music() {
|
||||
if (music_) {
|
||||
Mix_FreeMusic(music_);
|
||||
music_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Music::play(int loops) {
|
||||
if (!music_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AudioSystem::get().isInitialized()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio system not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
|
||||
Mix_VolumeMusic(vol);
|
||||
|
||||
return Mix_PlayMusic(music_, loops) == 0;
|
||||
}
|
||||
|
||||
bool Music::fadeIn(int ms, int loops) {
|
||||
if (!music_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AudioSystem::get().isInitialized()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio system not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
|
||||
Mix_VolumeMusic(vol);
|
||||
|
||||
return Mix_FadeInMusic(music_, loops, ms) == 0;
|
||||
}
|
||||
|
||||
void Music::pause() {
|
||||
if (!music_) {
|
||||
return;
|
||||
}
|
||||
Mix_PauseMusic();
|
||||
}
|
||||
|
||||
void Music::resume() {
|
||||
if (!music_) {
|
||||
return;
|
||||
}
|
||||
Mix_ResumeMusic();
|
||||
}
|
||||
|
||||
void Music::stop() {
|
||||
if (!music_) {
|
||||
return;
|
||||
}
|
||||
Mix_HaltMusic();
|
||||
}
|
||||
|
||||
bool Music::fadeOut(int ms) {
|
||||
if (!music_) {
|
||||
return false;
|
||||
}
|
||||
return Mix_FadeOutMusic(ms) == 0;
|
||||
}
|
||||
|
||||
void Music::setVolume(float volume) {
|
||||
volume_ = std::clamp(volume, 0.0f, 1.0f);
|
||||
|
||||
if (AudioSystem::get().isInitialized() && isPlaying()) {
|
||||
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
|
||||
Mix_VolumeMusic(vol);
|
||||
}
|
||||
}
|
||||
|
||||
float Music::getVolume() const {
|
||||
return volume_;
|
||||
}
|
||||
|
||||
bool Music::isPlaying() const {
|
||||
return Mix_PlayingMusic() == 1;
|
||||
}
|
||||
|
||||
bool Music::isPaused() const {
|
||||
return Mix_PausedMusic() == 1;
|
||||
}
|
||||
|
||||
}
|
||||
76
Frostbite2D/src/frostbite2D/audio/sound.cpp
Normal file
76
Frostbite2D/src/frostbite2D/audio/sound.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include <frostbite2D/audio/sound.h>
|
||||
#include <frostbite2D/audio/audio_system.h>
|
||||
#include <frostbite2D/utils/asset.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
Ptr<Sound> Sound::loadFromFile(const std::string& path) {
|
||||
auto& asset = Asset::get();
|
||||
auto data = asset.readFileToBytes(path);
|
||||
if (!data) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read sound file: %s", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto sound = loadFromMemory(data->data(), data->size());
|
||||
if (sound) {
|
||||
sound->path_ = path;
|
||||
}
|
||||
return sound;
|
||||
}
|
||||
|
||||
Ptr<Sound> Sound::loadFromMemory(const uint8* data, size_t size) {
|
||||
SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast<int>(size));
|
||||
if (!rw) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops from memory: %s", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Mix_Chunk* chunk = Mix_LoadWAV_RW(rw, 1);
|
||||
if (!chunk) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound from memory: %s", Mix_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Ptr<Sound>(new Sound(chunk));
|
||||
}
|
||||
|
||||
Sound::Sound(Mix_Chunk* chunk, const std::string& path)
|
||||
: chunk_(chunk), path_(path) {
|
||||
}
|
||||
|
||||
Sound::~Sound() {
|
||||
if (chunk_) {
|
||||
Mix_FreeChunk(chunk_);
|
||||
chunk_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int Sound::play(int loops, int channel) {
|
||||
if (!chunk_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!AudioSystem::get().isInitialized()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio system not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int vol = static_cast<int>(volume_ * AudioSystem::get().getSoundVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
|
||||
Mix_VolumeChunk(chunk_, vol);
|
||||
|
||||
return Mix_PlayChannel(channel, chunk_, loops);
|
||||
}
|
||||
|
||||
void Sound::setVolume(float volume) {
|
||||
volume_ = std::clamp(volume, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float Sound::getVolume() const {
|
||||
return volume_;
|
||||
}
|
||||
|
||||
}
|
||||
BIN
Game/assets/BackgroundMusic.mp3
Normal file
BIN
Game/assets/BackgroundMusic.mp3
Normal file
Binary file not shown.
@@ -13,6 +13,10 @@
|
||||
#include <frostbite2D/utils/pvf_archive.h>
|
||||
#include <frostbite2D/utils/script_parser.h>
|
||||
|
||||
#include <frostbite2D/audio/audio_system.h>
|
||||
#include <frostbite2D/audio/sound.h>
|
||||
#include <frostbite2D/audio/music.h>
|
||||
|
||||
using namespace frostbite2D;
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
@@ -92,6 +96,16 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
AudioSystem::get().init();
|
||||
AudioSystem::get().setMasterVolume(1.0f);
|
||||
AudioSystem::get().setSoundVolume(0.8f);
|
||||
AudioSystem::get().setMusicVolume(0.6f);
|
||||
|
||||
auto bgMusic = Music::loadFromFile("assets/BackgroundMusic.mp3");
|
||||
if (bgMusic) {
|
||||
bgMusic->play();
|
||||
}
|
||||
|
||||
app.run();
|
||||
|
||||
app.shutdown();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
add_requires("libsdl2", {configs = {shared = true,wayland = true}})
|
||||
add_requires("libsdl2_image")
|
||||
add_requires("libsdl2_mixer")
|
||||
add_requires("glm")
|
||||
|
||||
target("Frostbite2D")
|
||||
@@ -14,6 +15,7 @@ target("Frostbite2D")
|
||||
|
||||
add_packages("libsdl2")
|
||||
add_packages("libsdl2_image")
|
||||
add_packages("libsdl2_mixer")
|
||||
add_packages("glm")
|
||||
|
||||
-- 复制着色器文件到输出目录
|
||||
|
||||
@@ -3,6 +3,7 @@ set_toolchains("mingw")
|
||||
|
||||
add_requires("libsdl2", {configs = {shared = true}})
|
||||
add_requires("libsdl2_image", {configs = {shared = true}})
|
||||
add_requires("libsdl2_mixer", {configs = {shared = true}})
|
||||
add_requires("glm")
|
||||
|
||||
target("Frostbite2D")
|
||||
@@ -16,6 +17,7 @@ target("Frostbite2D")
|
||||
|
||||
add_packages("libsdl2")
|
||||
add_packages("libsdl2_image")
|
||||
add_packages("libsdl2_mixer")
|
||||
add_packages("glm")
|
||||
|
||||
-- 复制 assets 目录到输出目录
|
||||
@@ -31,11 +33,12 @@ target("Frostbite2D")
|
||||
print("Copy assets directory: " .. assets_dir .. " -> " .. target_assets_dir)
|
||||
end
|
||||
|
||||
-- 复制 SDL2 DLL (Windows 平台)
|
||||
-- 复制 SDL2 和 SDL2_mixer DLL (Windows 平台)
|
||||
if is_plat("mingw") or is_plat("windows") then
|
||||
local sdl2_lib = target:pkg("libsdl2")
|
||||
if sdl2_lib then
|
||||
local libfiles = sdl2_lib:get("libfiles")
|
||||
for _, pkg_name in ipairs({"libsdl2", "libsdl2_mixer"}) do
|
||||
local pkg = target:pkg(pkg_name)
|
||||
if pkg then
|
||||
local libfiles = pkg:get("libfiles")
|
||||
if libfiles then
|
||||
for _, libfile in ipairs(libfiles) do
|
||||
-- 查找 DLL 文件
|
||||
@@ -47,20 +50,23 @@ target("Frostbite2D")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 尝试从 xmake 包目录复制 SDL2.dll
|
||||
local sdl2_dll_paths = {
|
||||
-- 尝试从 xmake 包目录复制 SDL2 和 SDL2_mixer DLL
|
||||
local dll_paths = {
|
||||
path.join(os.getenv("USERPROFILE") or "", ".xmake/packages/l/libsdl2/**/bin/SDL2.dll"),
|
||||
path.join(os.getenv("USERPROFILE") or "", ".xmake/packages/l/libsdl2/**/lib/SDL2.dll"),
|
||||
path.join(os.getenv("USERPROFILE") or "", ".xmake/packages/l/libsdl2_mixer/**/bin/SDL2_mixer.dll"),
|
||||
path.join(os.getenv("USERPROFILE") or "", ".xmake/packages/l/libsdl2_mixer/**/lib/SDL2_mixer.dll"),
|
||||
}
|
||||
|
||||
for _, dll_pattern in ipairs(sdl2_dll_paths) do
|
||||
for _, dll_pattern in ipairs(dll_paths) do
|
||||
local dll_files = os.files(dll_pattern)
|
||||
for _, dll_file in ipairs(dll_files) do
|
||||
local target_dll = path.join(output_dir, "SDL2.dll")
|
||||
local target_dll = path.join(output_dir, path.filename(dll_file))
|
||||
if not os.isfile(target_dll) then
|
||||
os.cp(dll_file, target_dll)
|
||||
print("Copy SDL2.dll from: " .. dll_file)
|
||||
print("Copy DLL from: " .. dll_file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user