diff --git a/Frostbite2D/include/frostbite2D/audio/audio_system.h b/Frostbite2D/include/frostbite2D/audio/audio_system.h new file mode 100644 index 0000000..1154e58 --- /dev/null +++ b/Frostbite2D/include/frostbite2D/audio/audio_system.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +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; +}; + +} diff --git a/Frostbite2D/include/frostbite2D/audio/music.h b/Frostbite2D/include/frostbite2D/audio/music.h new file mode 100644 index 0000000..22cf045 --- /dev/null +++ b/Frostbite2D/include/frostbite2D/audio/music.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +namespace frostbite2D { + +/** + * @brief 音乐类 + * + * 用于加载和播放长音频文件(背景音乐),支持从文件或内存加载。 + * 支持 WAV、OGG、MP3 等格式。 + */ +class Music : public RefObject { +public: + static Ptr loadFromFile(const std::string& path); + static Ptr 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; +}; + +} diff --git a/Frostbite2D/include/frostbite2D/audio/sound.h b/Frostbite2D/include/frostbite2D/audio/sound.h new file mode 100644 index 0000000..787cae2 --- /dev/null +++ b/Frostbite2D/include/frostbite2D/audio/sound.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +namespace frostbite2D { + +/** + * @brief 音效类 + * + * 用于加载和播放短音频文件,支持从文件或内存加载。 + * 支持 WAV、OGG 等格式。 + */ +class Sound : public RefObject { +public: + static Ptr loadFromFile(const std::string& path); + static Ptr 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; +}; + +} diff --git a/Frostbite2D/src/frostbite2D/audio/audio_system.cpp b/Frostbite2D/src/frostbite2D/audio/audio_system.cpp new file mode 100644 index 0000000..c76c79a --- /dev/null +++ b/Frostbite2D/src/frostbite2D/audio/audio_system.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include + +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(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); +} + +} diff --git a/Frostbite2D/src/frostbite2D/audio/music.cpp b/Frostbite2D/src/frostbite2D/audio/music.cpp new file mode 100644 index 0000000..d706199 --- /dev/null +++ b/Frostbite2D/src/frostbite2D/audio/music.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include +#include + +namespace frostbite2D { + +Ptr 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::loadFromMemory(const uint8* data, size_t size) { + SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast(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(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(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(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(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; +} + +} diff --git a/Frostbite2D/src/frostbite2D/audio/sound.cpp b/Frostbite2D/src/frostbite2D/audio/sound.cpp new file mode 100644 index 0000000..63e5f56 --- /dev/null +++ b/Frostbite2D/src/frostbite2D/audio/sound.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include + +namespace frostbite2D { + +Ptr 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::loadFromMemory(const uint8* data, size_t size) { + SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast(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(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(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_; +} + +} diff --git a/Game/assets/BackgroundMusic.mp3 b/Game/assets/BackgroundMusic.mp3 new file mode 100644 index 0000000..98915bc Binary files /dev/null and b/Game/assets/BackgroundMusic.mp3 differ diff --git a/Game/src/main.cpp b/Game/src/main.cpp index f588756..a14a93e 100644 --- a/Game/src/main.cpp +++ b/Game/src/main.cpp @@ -13,6 +13,10 @@ #include #include +#include +#include +#include + 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(); diff --git a/platform/linux.lua b/platform/linux.lua index 7c440a3..9f79285 100644 --- a/platform/linux.lua +++ b/platform/linux.lua @@ -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") -- 复制着色器文件到输出目录 diff --git a/platform/mingw.lua b/platform/mingw.lua index 8af4ee2..e3347eb 100644 --- a/platform/mingw.lua +++ b/platform/mingw.lua @@ -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,36 +33,40 @@ 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") - if libfiles then - for _, libfile in ipairs(libfiles) do - -- 查找 DLL 文件 - if libfile:endswith(".dll") then - local target_dll = path.join(output_dir, path.filename(libfile)) - os.cp(libfile, target_dll) - print("Copy DLL: " .. path.filename(libfile)) + 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 文件 + if libfile:endswith(".dll") then + local target_dll = path.join(output_dir, path.filename(libfile)) + os.cp(libfile, target_dll) + print("Copy DLL: " .. path.filename(libfile)) + end 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