feat(音频): 添加从NPK加载音频文件的功能

实现SoundPackArchive系统用于管理NPK格式的音频资源包
为Music和Sound类添加loadFromNpk方法支持从NPK加载音频
新增音频播放控制功能如暂停、恢复、停止等
This commit is contained in:
2026-03-21 00:37:32 +08:00
parent 18111dae6b
commit 46ca534a19
8 changed files with 492 additions and 2 deletions

View File

@@ -17,6 +17,7 @@ class Music : public RefObject {
public:
static Ptr<Music> loadFromFile(const std::string& path);
static Ptr<Music> loadFromMemory(const uint8* data, size_t size);
static Ptr<Music> 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<uint8> data, const std::string& path = "");
Mix_Music* music_ = nullptr;
std::string path_;
std::vector<uint8> data_;
float volume_ = 1.0f;
};

View File

@@ -17,12 +17,23 @@ class Sound : public RefObject {
public:
static Ptr<Sound> loadFromFile(const std::string& path);
static Ptr<Sound> loadFromMemory(const uint8* data, size_t size);
static Ptr<Sound> 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;
};
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include <frostbite2D/types/type_alias.h>
#include <frostbite2D/resource/binary_reader.h>
#include <frostbite2D/resource/asset.h>
#include <optional>
#include <string>
#include <map>
#include <vector>
#include <list>
#include <memory>
namespace frostbite2D {
struct AudioRef {
std::string path;
std::string npkFile;
uint32 offset = 0;
uint32 size = 0;
bool loaded = false;
};
struct CachedAudioData {
std::vector<uint8> 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<AudioRef> getAudio(const std::string& path);
std::vector<std::string> listAudios() const;
std::optional<std::vector<uint8>> 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<std::string, AudioRef> audioIndex_;
std::map<std::string, CachedAudioData> audioCache_;
std::list<std::string> lruList_;
size_t maxCacheSize_ = 256 * 1024 * 1024;
size_t currentCacheSize_ = 0;
std::string defaultAudioPath_;
};
}

View File

@@ -1,6 +1,7 @@
#include <frostbite2D/audio/music.h>
#include <frostbite2D/audio/audio_system.h>
#include <frostbite2D/resource/asset.h>
#include <frostbite2D/resource/sound_pack_archive.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_mixer.h>
#include <algorithm>
@@ -21,7 +22,9 @@ Ptr<Music> Music::loadFromFile(const std::string& path) {
}
Ptr<Music> Music::loadFromMemory(const uint8* data, size_t size) {
SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast<int>(size));
std::vector<uint8> dataCopy(data, data + size);
SDL_RWops* rw = SDL_RWFromConstMem(dataCopy.data(), static_cast<int>(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> Music::loadFromMemory(const uint8* data, size_t size) {
return nullptr;
}
return Ptr<Music>(new Music(music));
return Ptr<Music>(new Music(music, std::move(dataCopy)));
}
Ptr<Music> 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<uint8> data, const std::string& path)
: music_(music), path_(path), data_(std::move(data)) {
}
Music::~Music() {
if (music_) {
Mix_FreeMusic(music_);

View File

@@ -1,6 +1,7 @@
#include <frostbite2D/audio/sound.h>
#include <frostbite2D/audio/audio_system.h>
#include <frostbite2D/resource/asset.h>
#include <frostbite2D/resource/sound_pack_archive.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_mixer.h>
#include <algorithm>
@@ -36,6 +37,35 @@ Ptr<Sound> Sound::loadFromMemory(const uint8* data, size_t size) {
return Ptr<Sound>(new Sound(chunk));
}
Ptr<Sound> 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);
}
}

View File

@@ -0,0 +1,259 @@
#include <frostbite2D/resource/sound_pack_archive.h>
#include <algorithm>
#include <cctype>
#include <SDL.h>
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<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
SDL_Log("Scanning %d sound NPK files...", static_cast<int>(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<uint32>(offset);
audio.size = static_cast<uint32>(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<AudioRef> SoundPackArchive::getAudio(const std::string& path) {
auto it = audioIndex_.find(normalizePath(path));
if (it == audioIndex_.end()) {
return std::nullopt;
}
return it->second;
}
std::vector<std::string> SoundPackArchive::listAudios() const {
std::vector<std::string> result;
result.reserve(audioIndex_.size());
for (const auto& pair : audioIndex_) {
result.push_back(pair.first);
}
return result;
}
std::optional<std::vector<uint8>> 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<uint8> 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<uint8> encrypted = reader.readBytes(256);
if (encrypted.size() < 256) {
return "";
}
std::vector<char> decrypted(256);
for (int i = 0; i < 256; ++i) {
decrypted[i] = static_cast<char>(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_;
}
}

Binary file not shown.

View File

@@ -18,6 +18,8 @@
#include <frostbite2D/audio/sound.h>
#include <frostbite2D/resource/npk_archive.h>
#include <frostbite2D/resource/sound_pack_archive.h>
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();