diff --git a/Frostbite2D/include/frostbite2D/audio/music.h b/Frostbite2D/include/frostbite2D/audio/music.h index 22cf045..cca3737 100644 --- a/Frostbite2D/include/frostbite2D/audio/music.h +++ b/Frostbite2D/include/frostbite2D/audio/music.h @@ -17,6 +17,7 @@ class Music : public RefObject { public: static Ptr loadFromFile(const std::string& path); static Ptr loadFromMemory(const uint8* data, size_t size); + static Ptr loadFromNpk(const std::string& audioPath); ~Music(); @@ -37,9 +38,11 @@ public: private: Music(Mix_Music* music, const std::string& path = ""); + Music(Mix_Music* music, std::vector data, const std::string& path = ""); Mix_Music* music_ = nullptr; std::string path_; + std::vector data_; float volume_ = 1.0f; }; diff --git a/Frostbite2D/include/frostbite2D/audio/sound.h b/Frostbite2D/include/frostbite2D/audio/sound.h index 787cae2..737afb7 100644 --- a/Frostbite2D/include/frostbite2D/audio/sound.h +++ b/Frostbite2D/include/frostbite2D/audio/sound.h @@ -17,12 +17,23 @@ class Sound : public RefObject { public: static Ptr loadFromFile(const std::string& path); static Ptr loadFromMemory(const uint8* data, size_t size); + static Ptr loadFromNpk(const std::string& audioPath); ~Sound(); int play(int loops = 0, int channel = -1); + int playLoop(int channel = -1); void setVolume(float volume); float getVolume() const; + int getLastChannel() const; + + static void stop(int channel); + static void stopAll(); + static bool isPlaying(int channel); + static void pause(int channel); + static void resume(int channel); + static void pauseAll(); + static void resumeAll(); const std::string& getPath() const { return path_; } @@ -32,6 +43,7 @@ private: Mix_Chunk* chunk_ = nullptr; std::string path_; float volume_ = 1.0f; + int lastChannel_ = -1; }; } diff --git a/Frostbite2D/include/frostbite2D/resource/sound_pack_archive.h b/Frostbite2D/include/frostbite2D/resource/sound_pack_archive.h new file mode 100644 index 0000000..0f015ad --- /dev/null +++ b/Frostbite2D/include/frostbite2D/resource/sound_pack_archive.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace frostbite2D { + +struct AudioRef { + std::string path; + std::string npkFile; + uint32 offset = 0; + uint32 size = 0; + bool loaded = false; +}; + +struct CachedAudioData { + std::vector data; + uint64 lastUseTime = 0; + size_t memoryUsage = 0; +}; + +class SoundPackArchive { +public: + static SoundPackArchive& get(); + + SoundPackArchive(const SoundPackArchive&) = delete; + SoundPackArchive& operator=(const SoundPackArchive&) = delete; + SoundPackArchive(SoundPackArchive&&) = delete; + SoundPackArchive& operator=(SoundPackArchive&&) = delete; + + void setSoundPackDirectory(const std::string& dir); + const std::string& getSoundPackDirectory() const; + + void init(); + void close(); + bool isOpen() const; + + bool hasAudio(const std::string& path) const; + std::optional getAudio(const std::string& path); + std::vector listAudios() const; + + std::optional> getAudioData(const AudioRef& audio); + + void setCacheSize(size_t maxBytes); + void clearCache(); + size_t getCacheUsage() const; + + void setDefaultAudio(const std::string& audioPath); + const std::string& getDefaultAudioPath() const; + +private: + SoundPackArchive() = default; + ~SoundPackArchive() = default; + + std::string normalizePath(const std::string& path) const; + void scanNpkFiles(); + bool parseNpkFile(const std::string& npkPath); + bool loadAudioData(AudioRef& audio); + void evictCacheIfNeeded(size_t requiredSize); + void updateCacheUsage(const std::string& audioPath); + std::string readNpkInfoString(BinaryReader& reader); + + static const uint8 NPK_KEY[256]; + + std::string soundPackDirectory_ = "SoundPacks"; + bool initialized_ = false; + std::map audioIndex_; + std::map audioCache_; + std::list lruList_; + size_t maxCacheSize_ = 256 * 1024 * 1024; + size_t currentCacheSize_ = 0; + std::string defaultAudioPath_; +}; + +} diff --git a/Frostbite2D/src/frostbite2D/audio/music.cpp b/Frostbite2D/src/frostbite2D/audio/music.cpp index bbe7f91..1b71265 100644 --- a/Frostbite2D/src/frostbite2D/audio/music.cpp +++ b/Frostbite2D/src/frostbite2D/audio/music.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -21,7 +22,9 @@ Ptr Music::loadFromFile(const std::string& path) { } Ptr Music::loadFromMemory(const uint8* data, size_t size) { - SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast(size)); + std::vector dataCopy(data, data + size); + + SDL_RWops* rw = SDL_RWFromConstMem(dataCopy.data(), static_cast(dataCopy.size())); if (!rw) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops from memory: %s", SDL_GetError()); return nullptr; @@ -33,13 +36,46 @@ Ptr Music::loadFromMemory(const uint8* data, size_t size) { return nullptr; } - return Ptr(new Music(music)); + return Ptr(new Music(music, std::move(dataCopy))); +} + +Ptr Music::loadFromNpk(const std::string& audioPath) { + SoundPackArchive& archive = SoundPackArchive::get(); + + if (!archive.isOpen()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized"); + return nullptr; + } + + auto audioOpt = archive.getAudio(audioPath); + if (!audioOpt) { + std::string defaultPath = archive.getDefaultAudioPath(); + if (!defaultPath.empty()) { + audioOpt = archive.getAudio(defaultPath); + } + if (!audioOpt) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio not found in NPK: %s", audioPath.c_str()); + return nullptr; + } + } + + auto dataOpt = archive.getAudioData(*audioOpt); + if (!dataOpt || dataOpt->empty()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get audio data for: %s", audioPath.c_str()); + return nullptr; + } + + return loadFromMemory(dataOpt->data(), dataOpt->size()); } Music::Music(Mix_Music* music, const std::string& path) : music_(music), path_(path) { } +Music::Music(Mix_Music* music, std::vector data, const std::string& path) + : music_(music), path_(path), data_(std::move(data)) { +} + Music::~Music() { if (music_) { Mix_FreeMusic(music_); diff --git a/Frostbite2D/src/frostbite2D/audio/sound.cpp b/Frostbite2D/src/frostbite2D/audio/sound.cpp index f9ff855..7113004 100644 --- a/Frostbite2D/src/frostbite2D/audio/sound.cpp +++ b/Frostbite2D/src/frostbite2D/audio/sound.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,35 @@ Ptr Sound::loadFromMemory(const uint8* data, size_t size) { return Ptr(new Sound(chunk)); } +Ptr Sound::loadFromNpk(const std::string& audioPath) { + SoundPackArchive& archive = SoundPackArchive::get(); + + if (!archive.isOpen()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized"); + return nullptr; + } + + auto audioOpt = archive.getAudio(audioPath); + if (!audioOpt) { + std::string defaultPath = archive.getDefaultAudioPath(); + if (!defaultPath.empty()) { + audioOpt = archive.getAudio(defaultPath); + } + if (!audioOpt) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio not found in NPK: %s", audioPath.c_str()); + return nullptr; + } + } + + auto dataOpt = archive.getAudioData(*audioOpt); + if (!dataOpt || dataOpt->empty()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get audio data for: %s", audioPath.c_str()); + return nullptr; + } + + return loadFromMemory(dataOpt->data(), dataOpt->size()); +} + Sound::Sound(Mix_Chunk* chunk, const std::string& path) : chunk_(chunk), path_(path) { } @@ -64,6 +94,8 @@ int Sound::play(int loops, int channel) { int result = Mix_PlayChannel(channel, chunk_, loops); if (result == -1) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play sound: %s", Mix_GetError()); + } else { + lastChannel_ = result; } return result; } @@ -76,4 +108,49 @@ float Sound::getVolume() const { return volume_; } +int Sound::getLastChannel() const { + return lastChannel_; +} + +int Sound::playLoop(int channel) { + return play(-1, channel); +} + +void Sound::stop(int channel) { + if (channel >= 0) { + Mix_HaltChannel(channel); + } +} + +void Sound::stopAll() { + Mix_HaltChannel(-1); +} + +bool Sound::isPlaying(int channel) { + if (channel < 0) { + return false; + } + return Mix_Playing(channel) == 1; +} + +void Sound::pause(int channel) { + if (channel >= 0) { + Mix_Pause(channel); + } +} + +void Sound::resume(int channel) { + if (channel >= 0) { + Mix_Resume(channel); + } +} + +void Sound::pauseAll() { + Mix_Pause(-1); +} + +void Sound::resumeAll() { + Mix_Resume(-1); +} + } diff --git a/Frostbite2D/src/frostbite2D/resource/sound_pack_archive.cpp b/Frostbite2D/src/frostbite2D/resource/sound_pack_archive.cpp new file mode 100644 index 0000000..64f8fc4 --- /dev/null +++ b/Frostbite2D/src/frostbite2D/resource/sound_pack_archive.cpp @@ -0,0 +1,259 @@ +#include +#include +#include +#include + +namespace frostbite2D { + +const uint8 SoundPackArchive::NPK_KEY[256] = { + 112, 117, 99, 104, 105, 107, 111, 110, 64, 110, 101, 111, 112, 108, 101, 32, + 100, 117, 110, 103, 101, 111, 110, 32, 97, 110, 100, 32, 102, 105, 103, 104, + 116, 101, 114, 32, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, + 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, + 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, + 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, + 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, + 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, + 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 0 +}; + +SoundPackArchive& SoundPackArchive::get() { + static SoundPackArchive instance; + return instance; +} + +void SoundPackArchive::setSoundPackDirectory(const std::string& dir) { + soundPackDirectory_ = dir; +} + +const std::string& SoundPackArchive::getSoundPackDirectory() const { + return soundPackDirectory_; +} + +void SoundPackArchive::init() { + if (initialized_) { + close(); + } + + scanNpkFiles(); + initialized_ = true; +} + +void SoundPackArchive::close() { + audioIndex_.clear(); + audioCache_.clear(); + lruList_.clear(); + currentCacheSize_ = 0; + initialized_ = false; +} + +bool SoundPackArchive::isOpen() const { + return initialized_; +} + +std::string SoundPackArchive::normalizePath(const std::string& path) const { + std::string result = path; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +void SoundPackArchive::scanNpkFiles() { + Asset& asset = Asset::get(); + std::string npkDir = asset.resolvePath(soundPackDirectory_); + if (!asset.isDirectory(npkDir)) { + SDL_Log("Sound NPK directory not found: %s", npkDir.c_str()); + return; + } + + std::vector files = asset.listFilesWithExtension(npkDir, ".npk"); + SDL_Log("Scanning %d sound NPK files...", static_cast(files.size())); + for (const auto &file : files) { + parseNpkFile(file); + } +} + +bool SoundPackArchive::parseNpkFile(const std::string& npkPath) { + Asset& asset = Asset::get(); + BinaryReader reader(npkPath); + + if (!reader.isOpen()) { + SDL_Log("Failed to open sound NPK file: %s", npkPath.c_str()); + return false; + } + + std::string npkFileName = asset.getFileName(npkPath); + std::string header = reader.readNullTerminatedString(); + + if (header.find("NeoplePack_Bill") == std::string::npos) { + return false; + } + + int32 audioCount = reader.readInt32(); + + for (int32 i = 0; i < audioCount; ++i) { + int32 offset = reader.readInt32(); + int32 length = reader.readInt32(); + std::string audioPath = readNpkInfoString(reader); + + AudioRef audio; + audio.path = normalizePath(audioPath); + audio.npkFile = npkFileName; + audio.offset = static_cast(offset); + audio.size = static_cast(length); + audio.loaded = false; + + audioIndex_[audio.path] = audio; + } + + return true; +} + +bool SoundPackArchive::hasAudio(const std::string& path) const { + return audioIndex_.find(normalizePath(path)) != audioIndex_.end(); +} + +std::optional SoundPackArchive::getAudio(const std::string& path) { + auto it = audioIndex_.find(normalizePath(path)); + if (it == audioIndex_.end()) { + return std::nullopt; + } + return it->second; +} + +std::vector SoundPackArchive::listAudios() const { + std::vector result; + result.reserve(audioIndex_.size()); + for (const auto& pair : audioIndex_) { + result.push_back(pair.first); + } + return result; +} + +std::optional> SoundPackArchive::getAudioData(const AudioRef& audio) { + std::string audioPath = normalizePath(audio.path); + + auto cacheIt = audioCache_.find(audioPath); + if (cacheIt == audioCache_.end()) { + auto it = audioIndex_.find(audioPath); + if (it == audioIndex_.end()) { + return std::nullopt; + } + + if (!loadAudioData(it->second)) { + return std::nullopt; + } + cacheIt = audioCache_.find(audioPath); + if (cacheIt == audioCache_.end()) { + return std::nullopt; + } + } + + updateCacheUsage(audioPath); + return cacheIt->second.data; +} + +bool SoundPackArchive::loadAudioData(AudioRef& audio) { + Asset& asset = Asset::get(); + std::string npkPath = asset.combinePath(soundPackDirectory_, audio.npkFile); + npkPath = asset.resolvePath(npkPath); + + BinaryReader reader(npkPath); + if (!reader.isOpen()) { + SDL_Log("Failed to open NPK for audio: %s", npkPath.c_str()); + return false; + } + + reader.seek(audio.offset); + std::vector audioData = reader.readBytes(audio.size); + + if (audioData.size() != audio.size) { + SDL_Log("Failed to read complete audio data"); + return false; + } + + evictCacheIfNeeded(audioData.size()); + + CachedAudioData cachedData; + cachedData.data = std::move(audioData); + cachedData.memoryUsage = cachedData.data.size(); + cachedData.lastUseTime = SDL_GetTicks(); + + audioCache_[audio.path] = std::move(cachedData); + currentCacheSize_ += audioCache_[audio.path].memoryUsage; + lruList_.push_front(audio.path); + + audio.loaded = true; + audioIndex_[audio.path] = audio; + + return true; +} + +void SoundPackArchive::setCacheSize(size_t maxBytes) { + maxCacheSize_ = maxBytes; + evictCacheIfNeeded(0); +} + +void SoundPackArchive::clearCache() { + audioCache_.clear(); + lruList_.clear(); + currentCacheSize_ = 0; +} + +size_t SoundPackArchive::getCacheUsage() const { + return currentCacheSize_; +} + +void SoundPackArchive::evictCacheIfNeeded(size_t requiredSize) { + while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) { + std::string oldest = lruList_.back(); + lruList_.pop_back(); + + auto it = audioCache_.find(oldest); + if (it != audioCache_.end()) { + currentCacheSize_ -= it->second.memoryUsage; + audioCache_.erase(it); + } + } +} + +void SoundPackArchive::updateCacheUsage(const std::string& audioPath) { + auto it = std::find(lruList_.begin(), lruList_.end(), audioPath); + if (it != lruList_.end()) { + lruList_.erase(it); + } + lruList_.push_front(audioPath); + + auto cacheIt = audioCache_.find(audioPath); + if (cacheIt != audioCache_.end()) { + cacheIt->second.lastUseTime = SDL_GetTicks(); + } +} + +std::string SoundPackArchive::readNpkInfoString(BinaryReader& reader) { + if (reader.eof()) { + return ""; + } + + std::vector encrypted = reader.readBytes(256); + if (encrypted.size() < 256) { + return ""; + } + + std::vector decrypted(256); + for (int i = 0; i < 256; ++i) { + decrypted[i] = static_cast(encrypted[i] ^ NPK_KEY[i]); + } + + return std::string(decrypted.data()); +} + +void SoundPackArchive::setDefaultAudio(const std::string& audioPath) { + defaultAudioPath_ = normalizePath(audioPath); +} + +const std::string& SoundPackArchive::getDefaultAudioPath() const { + return defaultAudioPath_; +} + +} diff --git a/Game/assets/SoundPacks/sounds_ui.npk b/Game/assets/SoundPacks/sounds_ui.npk new file mode 100644 index 0000000..dc3535d Binary files /dev/null and b/Game/assets/SoundPacks/sounds_ui.npk differ diff --git a/Game/src/main.cpp b/Game/src/main.cpp index 4a59ad3..eb80e86 100644 --- a/Game/src/main.cpp +++ b/Game/src/main.cpp @@ -18,6 +18,8 @@ #include #include +#include + using namespace frostbite2D; int main(int argc, char **argv) { @@ -117,11 +119,30 @@ int main(int argc, char **argv) { auto sprite1 = Sprite::createFromNpk("sprite/newtitle/nangua.img", 0); if (sprite1) { sprite1->SetPosition(0, 0); + sprite1->SetScale(5.0f); menuScene->AddActor(sprite1); } else { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create sprite from NPK!"); } + SoundPackArchive &archive = SoundPackArchive::get(); + archive.setSoundPackDirectory("assets/SoundPacks"); + archive.init(); + + // auto sound = Sound::loadFromNpk("sounds/ui/adventurer_maker_name.ogg"); + // if (sound) { + // sound->play(); + // } else { + // SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound!"); + // } + + auto music = Music::loadFromNpk("sounds/ui/amazing_box.ogg"); + if (music) { + music->play(); + } else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load music!"); + } + app.run(); app.shutdown();