feat(audio): 添加音频数据库支持及XML解析功能

实现音频数据库系统,支持从XML文件加载音频配置
添加RapidXML库用于XML解析
提供统一的音频查询接口,支持音效、音乐和随机音效组
This commit is contained in:
2026-03-21 02:44:29 +08:00
parent 46ca534a19
commit 4870627b4d
8 changed files with 23258 additions and 0 deletions

View File

@@ -0,0 +1,323 @@
#pragma once
#include <frostbite2D/types/type_alias.h>
#include <optional>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
namespace frostbite2D {
/**
* @brief 音频项类型枚举
*/
enum class AudioEntryType {
None, ///< 无效
Effect, ///< 音效
Music, ///< 背景音乐
Random ///< 随机音效组
};
/**
* @brief 音效配置
*/
struct AudioEffect {
std::string id; ///< 音效 ID
std::string file; ///< 文件路径
std::string subgroup; ///< 子组(可选)
int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置)
int32 loopDelayRange = -1;///< 循环延迟范围(-1 表示未设置)
};
/**
* @brief 音乐配置
*/
struct AudioMusic {
std::string id; ///< 音乐 ID
std::string file; ///< 文件路径
int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置)
};
/**
* @brief 随机音效项
*/
struct RandomItem {
std::string tag; ///< 音效标签ID
int32 probability = 0; ///< 概率权重
};
/**
* @brief 随机音效组
*/
struct AudioRandom {
std::string id; ///< 随机组 ID
std::vector<RandomItem> items; ///< 随机项列表
};
/**
* @brief 统一的音频条目(包含类型和数据)
*/
struct AudioEntry {
AudioEntryType type = AudioEntryType::None; ///< 条目类型
const AudioEffect* effect = nullptr; ///< 音效指针type == Effect 时有效)
const AudioMusic* music = nullptr; ///< 音乐指针type == Music 时有效)
const AudioRandom* random = nullptr; ///< 随机组指针type == Random 时有效)
/// @brief 判断是否有效
bool isValid() const { return type != AudioEntryType::None; }
/// @brief 判断是否为音效
bool isEffect() const { return type == AudioEntryType::Effect; }
/// @brief 判断是否为音乐
bool isMusic() const { return type == AudioEntryType::Music; }
/// @brief 判断是否为随机组
bool isRandom() const { return type == AudioEntryType::Random; }
/// @brief 安全获取文件路径
std::string getFilePath() const {
if (type == AudioEntryType::Effect && effect)
return effect->file;
if (type == AudioEntryType::Music && music)
return music->file;
return "";
}
/// @brief 安全获取子组
std::string getSubgroup() const {
if (type == AudioEntryType::Effect && effect)
return effect->subgroup;
return "";
}
};
/**
* @brief 音频数据库(单例)
*
* 用于解析和访问 audio.xml 文件中的音频配置。
* 提供统一的查询接口,自动判断音频类型。
*
* @example
* auto& audioDB = AudioDatabase::get();
* if (audioDB.loadFromFile("audio.xml")) {
* // 统一查询
* if (auto entry = audioDB.get("GN_FINAL_SHOT_01")) {
* // 使用 entry
* }
*
* // 获取文件路径
* if (auto filePath = audioDB.getFilePath("GN_FINAL_SHOT_01")) {
* // 使用 *filePath
* }
*
* // 获取音效(自动处理随机组)
* if (auto soundId = audioDB.getSound("R_GN_FINAL_SHOT")) {
* // 使用 *soundId
* }
* }
*/
class AudioDatabase {
public:
/**
* @brief 获取单例实例
* @return 音频数据库实例引用
*/
static AudioDatabase& get();
AudioDatabase(const AudioDatabase&) = delete;
AudioDatabase& operator=(const AudioDatabase&) = delete;
// ---------------------------------------------------------------------------
// 加载和解析
// ---------------------------------------------------------------------------
/**
* @brief 从文件加载音频数据库
* @param path XML 文件路径
* @return 加载成功返回 true
*/
bool loadFromFile(const std::string& path);
/**
* @brief 从字符串加载音频数据库
* @param xmlContent XML 内容字符串
* @return 加载成功返回 true
*/
bool loadFromString(const std::string& xmlContent);
/**
* @brief 清空数据库
*/
void clear();
/**
* @brief 检查数据库是否已加载
* @return 已加载返回 true
*/
bool isLoaded() const;
// ---------------------------------------------------------------------------
// 统一查询接口
// ---------------------------------------------------------------------------
/**
* @brief 统一查询:输入任意 ID自动判断类型
* @param id 音频 ID可以是 EFFECT、MUSIC 或 RANDOM 的 ID
* @return 音频条目,未找到返回 std::nullopt
*/
std::optional<AudioEntry> get(const std::string& id);
/**
* @brief 获取音效 ID自动处理 RANDOM 类型)
*
* 如果 id 是 RANDOM 类型,会按概率随机选择一个音效;
* 如果 id 是 EFFECT 类型,直接返回该 ID。
*
* @param id 音效 ID 或随机组 ID
* @return 音效 ID未找到返回 std::nullopt
*/
std::optional<std::string> getSound(const std::string& id);
/**
* @brief 获取文件路径
*
* 对于 EFFECT 和 MUSIC 类型,返回对应的文件路径;
* 对于 RANDOM 类型,先随机选择一个音效再返回其文件路径。
*
* @param id 音频 ID
* @return 文件路径,未找到返回 std::nullopt
*/
std::optional<std::string> getFilePath(const std::string& id);
// ---------------------------------------------------------------------------
// 类型特定查询(高级用法)
// ---------------------------------------------------------------------------
/**
* @brief 获取音效配置
* @param id 音效 ID
* @return 音效配置指针,未找到返回 std::nullopt
*/
std::optional<const AudioEffect*> getEffect(const std::string& id);
/**
* @brief 获取音乐配置
* @param id 音乐 ID
* @return 音乐配置指针,未找到返回 std::nullopt
*/
std::optional<const AudioMusic*> getMusic(const std::string& id);
/**
* @brief 获取随机音效组配置
* @param id 随机组 ID
* @return 随机组配置指针,未找到返回 std::nullopt
*/
std::optional<const AudioRandom*> getRandom(const std::string& id);
// ---------------------------------------------------------------------------
// 统计信息
// ---------------------------------------------------------------------------
/**
* @brief 获取音效数量
*/
size_t effectCount() const;
/**
* @brief 获取音乐数量
*/
size_t musicCount() const;
/**
* @brief 获取随机组数量
*/
size_t randomCount() const;
// ---------------------------------------------------------------------------
// 便捷访问方法(推荐使用)
// ---------------------------------------------------------------------------
/**
* @brief 检查 ID 是否存在
* @param id 音频 ID
* @return 存在返回 true
*/
bool has(const std::string& id);
/**
* @brief 获取文件路径(不存在返回空字符串)
* @param id 音频 ID
* @return 文件路径,未找到返回空字符串
*/
std::string filePath(const std::string& id);
/**
* @brief 获取音效 ID自动处理随机组不存在返回空字符串
* @param idOrRandomId 音效 ID 或随机组 ID
* @return 音效 ID未找到返回空字符串
*/
std::string soundId(const std::string& idOrRandomId);
/**
* @brief 获取子组EFFECT 的 SUBGROUP 属性)
* @param id 音频 ID
* @return 子组名,未找到或不存在返回空字符串
*/
std::string subgroup(const std::string& id);
/**
* @brief 获取条目类型
* @param id 音频 ID
* @return 条目类型,未找到返回 AudioEntryType::None
*/
AudioEntryType typeOf(const std::string& id);
// ---------------------------------------------------------------------------
// 带检查的访问方法
// ---------------------------------------------------------------------------
/**
* @brief 尝试获取文件路径,带成功标志
* @param id 音频 ID
* @param outPath 输出文件路径
* @return 成功返回 true
*/
bool tryGetFilePath(const std::string& id, std::string& outPath);
/**
* @brief 尝试获取音效 ID带成功标志
* @param id 音效 ID 或随机组 ID
* @param outSoundId 输出音效 ID
* @return 成功返回 true
*/
bool tryGetSoundId(const std::string& id, std::string& outSoundId);
private:
AudioDatabase() = default;
~AudioDatabase() = default;
/**
* @brief 初始化随机数生成器
*/
void initializeRNG();
/**
* @brief 从随机组中按概率选择一个音效
* @param randomGroup 随机组配置
* @return 选中的音效 ID
*/
std::string selectFromRandom(const AudioRandom& randomGroup);
std::unordered_map<std::string, AudioEffect> effectMap_; ///< 音效映射
std::unordered_map<std::string, AudioMusic> musicMap_; ///< 音乐映射
std::unordered_map<std::string, AudioRandom> randomMap_; ///< 随机组映射
std::mt19937 rng_; ///< 随机数生成器
bool rngInitialized_ = false; ///< 随机数生成器是否已初始化
bool loaded_ = false;
};
} // namespace frostbite2D

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
#ifndef RAPIDXML_ITERATORS_HPP_INCLUDED
#define RAPIDXML_ITERATORS_HPP_INCLUDED
// Copyright (C) 2006, 2009 Marcin Kalicinski
// Version 1.13
// Revision $DateTime: 2009/05/13 01:46:17 $
//! \file rapidxml_iterators.hpp This file contains rapidxml iterators
#include "rapidxml.hpp"
namespace rapidxml
{
//! Iterator of child nodes of xml_node
template<class Ch>
class node_iterator
{
public:
typedef typename xml_node<Ch> value_type;
typedef typename xml_node<Ch> &reference;
typedef typename xml_node<Ch> *pointer;
typedef std::ptrdiff_t difference_type;
typedef std::bidirectional_iterator_tag iterator_category;
node_iterator()
: m_node(0)
{
}
node_iterator(xml_node<Ch> *node)
: m_node(node->first_node())
{
}
reference operator *() const
{
assert(m_node);
return *m_node;
}
pointer operator->() const
{
assert(m_node);
return m_node;
}
node_iterator& operator++()
{
assert(m_node);
m_node = m_node->next_sibling();
return *this;
}
node_iterator operator++(int)
{
node_iterator tmp = *this;
++this;
return tmp;
}
node_iterator& operator--()
{
assert(m_node && m_node->previous_sibling());
m_node = m_node->previous_sibling();
return *this;
}
node_iterator operator--(int)
{
node_iterator tmp = *this;
++this;
return tmp;
}
bool operator ==(const node_iterator<Ch> &rhs)
{
return m_node == rhs.m_node;
}
bool operator !=(const node_iterator<Ch> &rhs)
{
return m_node != rhs.m_node;
}
private:
xml_node<Ch> *m_node;
};
//! Iterator of child attributes of xml_node
template<class Ch>
class attribute_iterator
{
public:
typedef typename xml_attribute<Ch> value_type;
typedef typename xml_attribute<Ch> &reference;
typedef typename xml_attribute<Ch> *pointer;
typedef std::ptrdiff_t difference_type;
typedef std::bidirectional_iterator_tag iterator_category;
attribute_iterator()
: m_attribute(0)
{
}
attribute_iterator(xml_node<Ch> *node)
: m_attribute(node->first_attribute())
{
}
reference operator *() const
{
assert(m_attribute);
return *m_attribute;
}
pointer operator->() const
{
assert(m_attribute);
return m_attribute;
}
attribute_iterator& operator++()
{
assert(m_attribute);
m_attribute = m_attribute->next_attribute();
return *this;
}
attribute_iterator operator++(int)
{
attribute_iterator tmp = *this;
++this;
return tmp;
}
attribute_iterator& operator--()
{
assert(m_attribute && m_attribute->previous_attribute());
m_attribute = m_attribute->previous_attribute();
return *this;
}
attribute_iterator operator--(int)
{
attribute_iterator tmp = *this;
++this;
return tmp;
}
bool operator ==(const attribute_iterator<Ch> &rhs)
{
return m_attribute == rhs.m_attribute;
}
bool operator !=(const attribute_iterator<Ch> &rhs)
{
return m_attribute != rhs.m_attribute;
}
private:
xml_attribute<Ch> *m_attribute;
};
}
#endif

View File

@@ -0,0 +1,421 @@
#ifndef RAPIDXML_PRINT_HPP_INCLUDED
#define RAPIDXML_PRINT_HPP_INCLUDED
// Copyright (C) 2006, 2009 Marcin Kalicinski
// Version 1.13
// Revision $DateTime: 2009/05/13 01:46:17 $
//! \file rapidxml_print.hpp This file contains rapidxml printer implementation
#include "rapidxml.hpp"
// Only include streams if not disabled
#ifndef RAPIDXML_NO_STREAMS
#include <ostream>
#include <iterator>
#endif
namespace rapidxml
{
///////////////////////////////////////////////////////////////////////
// Printing flags
const int print_no_indenting = 0x1; //!< Printer flag instructing the printer to suppress indenting of XML. See print() function.
///////////////////////////////////////////////////////////////////////
// Internal
//! \cond internal
namespace internal
{
///////////////////////////////////////////////////////////////////////////
// Internal character operations
// Copy characters from given range to given output iterator
template<class OutIt, class Ch>
inline OutIt copy_chars(const Ch *begin, const Ch *end, OutIt out)
{
while (begin != end)
*out++ = *begin++;
return out;
}
// Copy characters from given range to given output iterator and expand
// characters into references (&lt; &gt; &apos; &quot; &amp;)
template<class OutIt, class Ch>
inline OutIt copy_and_expand_chars(const Ch *begin, const Ch *end, Ch noexpand, OutIt out)
{
while (begin != end)
{
if (*begin == noexpand)
{
*out++ = *begin; // No expansion, copy character
}
else
{
switch (*begin)
{
case Ch('<'):
*out++ = Ch('&'); *out++ = Ch('l'); *out++ = Ch('t'); *out++ = Ch(';');
break;
case Ch('>'):
*out++ = Ch('&'); *out++ = Ch('g'); *out++ = Ch('t'); *out++ = Ch(';');
break;
case Ch('\''):
*out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('p'); *out++ = Ch('o'); *out++ = Ch('s'); *out++ = Ch(';');
break;
case Ch('"'):
*out++ = Ch('&'); *out++ = Ch('q'); *out++ = Ch('u'); *out++ = Ch('o'); *out++ = Ch('t'); *out++ = Ch(';');
break;
case Ch('&'):
*out++ = Ch('&'); *out++ = Ch('a'); *out++ = Ch('m'); *out++ = Ch('p'); *out++ = Ch(';');
break;
default:
*out++ = *begin; // No expansion, copy character
}
}
++begin; // Step to next character
}
return out;
}
// Fill given output iterator with repetitions of the same character
template<class OutIt, class Ch>
inline OutIt fill_chars(OutIt out, int n, Ch ch)
{
for (int i = 0; i < n; ++i)
*out++ = ch;
return out;
}
// Find character
template<class Ch, Ch ch>
inline bool find_char(const Ch *begin, const Ch *end)
{
while (begin != end)
if (*begin++ == ch)
return true;
return false;
}
///////////////////////////////////////////////////////////////////////////
// Internal printing operations
// Print node
template<class OutIt, class Ch>
inline OutIt print_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
// Print proper node type
switch (node->type())
{
// Document
case node_document:
out = print_children(out, node, flags, indent);
break;
// Element
case node_element:
out = print_element_node(out, node, flags, indent);
break;
// Data
case node_data:
out = print_data_node(out, node, flags, indent);
break;
// CDATA
case node_cdata:
out = print_cdata_node(out, node, flags, indent);
break;
// Declaration
case node_declaration:
out = print_declaration_node(out, node, flags, indent);
break;
// Comment
case node_comment:
out = print_comment_node(out, node, flags, indent);
break;
// Doctype
case node_doctype:
out = print_doctype_node(out, node, flags, indent);
break;
// Pi
case node_pi:
out = print_pi_node(out, node, flags, indent);
break;
// Unknown
default:
assert(0);
break;
}
// If indenting not disabled, add line break after node
if (!(flags & print_no_indenting))
*out = Ch('\n'), ++out;
// Return modified iterator
return out;
}
// Print children of the node
template<class OutIt, class Ch>
inline OutIt print_children(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
for (xml_node<Ch> *child = node->first_node(); child; child = child->next_sibling())
out = print_node(out, child, flags, indent);
return out;
}
// Print attributes of the node
template<class OutIt, class Ch>
inline OutIt print_attributes(OutIt out, const xml_node<Ch> *node, int flags)
{
for (xml_attribute<Ch> *attribute = node->first_attribute(); attribute; attribute = attribute->next_attribute())
{
if (attribute->name() && attribute->value())
{
// Print attribute name
*out = Ch(' '), ++out;
out = copy_chars(attribute->name(), attribute->name() + attribute->name_size(), out);
*out = Ch('='), ++out;
// Print attribute value using appropriate quote type
if (find_char<Ch, Ch('"')>(attribute->value(), attribute->value() + attribute->value_size()))
{
*out = Ch('\''), ++out;
out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('"'), out);
*out = Ch('\''), ++out;
}
else
{
*out = Ch('"'), ++out;
out = copy_and_expand_chars(attribute->value(), attribute->value() + attribute->value_size(), Ch('\''), out);
*out = Ch('"'), ++out;
}
}
}
return out;
}
// Print data node
template<class OutIt, class Ch>
inline OutIt print_data_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
assert(node->type() == node_data);
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out);
return out;
}
// Print data node
template<class OutIt, class Ch>
inline OutIt print_cdata_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
assert(node->type() == node_cdata);
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
*out = Ch('<'); ++out;
*out = Ch('!'); ++out;
*out = Ch('['); ++out;
*out = Ch('C'); ++out;
*out = Ch('D'); ++out;
*out = Ch('A'); ++out;
*out = Ch('T'); ++out;
*out = Ch('A'); ++out;
*out = Ch('['); ++out;
out = copy_chars(node->value(), node->value() + node->value_size(), out);
*out = Ch(']'); ++out;
*out = Ch(']'); ++out;
*out = Ch('>'); ++out;
return out;
}
// Print element node
template<class OutIt, class Ch>
inline OutIt print_element_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
assert(node->type() == node_element);
// Print element name and attributes, if any
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
*out = Ch('<'), ++out;
out = copy_chars(node->name(), node->name() + node->name_size(), out);
out = print_attributes(out, node, flags);
// If node is childless
if (node->value_size() == 0 && !node->first_node())
{
// Print childless node tag ending
*out = Ch('/'), ++out;
*out = Ch('>'), ++out;
}
else
{
// Print normal node tag ending
*out = Ch('>'), ++out;
// Test if node contains a single data node only (and no other nodes)
xml_node<Ch> *child = node->first_node();
if (!child)
{
// If node has no children, only print its value without indenting
out = copy_and_expand_chars(node->value(), node->value() + node->value_size(), Ch(0), out);
}
else if (child->next_sibling() == 0 && child->type() == node_data)
{
// If node has a sole data child, only print its value without indenting
out = copy_and_expand_chars(child->value(), child->value() + child->value_size(), Ch(0), out);
}
else
{
// Print all children with full indenting
if (!(flags & print_no_indenting))
*out = Ch('\n'), ++out;
out = print_children(out, node, flags, indent + 1);
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
}
// Print node end
*out = Ch('<'), ++out;
*out = Ch('/'), ++out;
out = copy_chars(node->name(), node->name() + node->name_size(), out);
*out = Ch('>'), ++out;
}
return out;
}
// Print declaration node
template<class OutIt, class Ch>
inline OutIt print_declaration_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
// Print declaration start
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
*out = Ch('<'), ++out;
*out = Ch('?'), ++out;
*out = Ch('x'), ++out;
*out = Ch('m'), ++out;
*out = Ch('l'), ++out;
// Print attributes
out = print_attributes(out, node, flags);
// Print declaration end
*out = Ch('?'), ++out;
*out = Ch('>'), ++out;
return out;
}
// Print comment node
template<class OutIt, class Ch>
inline OutIt print_comment_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
assert(node->type() == node_comment);
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
*out = Ch('<'), ++out;
*out = Ch('!'), ++out;
*out = Ch('-'), ++out;
*out = Ch('-'), ++out;
out = copy_chars(node->value(), node->value() + node->value_size(), out);
*out = Ch('-'), ++out;
*out = Ch('-'), ++out;
*out = Ch('>'), ++out;
return out;
}
// Print doctype node
template<class OutIt, class Ch>
inline OutIt print_doctype_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
assert(node->type() == node_doctype);
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
*out = Ch('<'), ++out;
*out = Ch('!'), ++out;
*out = Ch('D'), ++out;
*out = Ch('O'), ++out;
*out = Ch('C'), ++out;
*out = Ch('T'), ++out;
*out = Ch('Y'), ++out;
*out = Ch('P'), ++out;
*out = Ch('E'), ++out;
*out = Ch(' '), ++out;
out = copy_chars(node->value(), node->value() + node->value_size(), out);
*out = Ch('>'), ++out;
return out;
}
// Print pi node
template<class OutIt, class Ch>
inline OutIt print_pi_node(OutIt out, const xml_node<Ch> *node, int flags, int indent)
{
assert(node->type() == node_pi);
if (!(flags & print_no_indenting))
out = fill_chars(out, indent, Ch('\t'));
*out = Ch('<'), ++out;
*out = Ch('?'), ++out;
out = copy_chars(node->name(), node->name() + node->name_size(), out);
*out = Ch(' '), ++out;
out = copy_chars(node->value(), node->value() + node->value_size(), out);
*out = Ch('?'), ++out;
*out = Ch('>'), ++out;
return out;
}
}
//! \endcond
///////////////////////////////////////////////////////////////////////////
// Printing
//! Prints XML to given output iterator.
//! \param out Output iterator to print to.
//! \param node Node to be printed. Pass xml_document to print entire document.
//! \param flags Flags controlling how XML is printed.
//! \return Output iterator pointing to position immediately after last character of printed text.
template<class OutIt, class Ch>
inline OutIt print(OutIt out, const xml_node<Ch> &node, int flags = 0)
{
return internal::print_node(out, &node, flags, 0);
}
#ifndef RAPIDXML_NO_STREAMS
//! Prints XML to given output stream.
//! \param out Output stream to print to.
//! \param node Node to be printed. Pass xml_document to print entire document.
//! \param flags Flags controlling how XML is printed.
//! \return Output stream.
template<class Ch>
inline std::basic_ostream<Ch> &print(std::basic_ostream<Ch> &out, const xml_node<Ch> &node, int flags = 0)
{
print(std::ostream_iterator<Ch>(out), node, flags);
return out;
}
//! Prints formatted XML to given output stream. Uses default printing flags. Use print() function to customize printing process.
//! \param out Output stream to print to.
//! \param node Node to be printed.
//! \return Output stream.
template<class Ch>
inline std::basic_ostream<Ch> &operator <<(std::basic_ostream<Ch> &out, const xml_node<Ch> &node)
{
return print(out, node);
}
#endif
}
#endif

View File

@@ -0,0 +1,122 @@
#ifndef RAPIDXML_UTILS_HPP_INCLUDED
#define RAPIDXML_UTILS_HPP_INCLUDED
// Copyright (C) 2006, 2009 Marcin Kalicinski
// Version 1.13
// Revision $DateTime: 2009/05/13 01:46:17 $
//! \file rapidxml_utils.hpp This file contains high-level rapidxml utilities that can be useful
//! in certain simple scenarios. They should probably not be used if maximizing performance is the main objective.
#include "rapidxml.hpp"
#include <vector>
#include <string>
#include <fstream>
#include <stdexcept>
namespace rapidxml
{
//! Represents data loaded from a file
template<class Ch = char>
class file
{
public:
//! Loads file into the memory. Data will be automatically destroyed by the destructor.
//! \param filename Filename to load.
file(const char *filename)
{
using namespace std;
// Open stream
basic_ifstream<Ch> stream(filename, ios::binary);
if (!stream)
throw runtime_error(string("cannot open file ") + filename);
stream.unsetf(ios::skipws);
// Determine stream size
stream.seekg(0, ios::end);
size_t size = stream.tellg();
stream.seekg(0);
// Load data and add terminating 0
m_data.resize(size + 1);
stream.read(&m_data.front(), static_cast<streamsize>(size));
m_data[size] = 0;
}
//! Loads file into the memory. Data will be automatically destroyed by the destructor
//! \param stream Stream to load from
file(std::basic_istream<Ch> &stream)
{
using namespace std;
// Load data and add terminating 0
stream.unsetf(ios::skipws);
m_data.assign(istreambuf_iterator<Ch>(stream), istreambuf_iterator<Ch>());
if (stream.fail() || stream.bad())
throw runtime_error("error reading stream");
m_data.push_back(0);
}
//! Gets file data.
//! \return Pointer to data of file.
Ch *data()
{
return &m_data.front();
}
//! Gets file data.
//! \return Pointer to data of file.
const Ch *data() const
{
return &m_data.front();
}
//! Gets file data size.
//! \return Size of file data, in characters.
std::size_t size() const
{
return m_data.size();
}
private:
std::vector<Ch> m_data; // File data
};
//! Counts children of node. Time complexity is O(n).
//! \return Number of children of node
template<class Ch>
inline std::size_t count_children(xml_node<Ch> *node)
{
xml_node<Ch> *child = node->first_node();
std::size_t count = 0;
while (child)
{
++count;
child = child->next_sibling();
}
return count;
}
//! Counts attributes of node. Time complexity is O(n).
//! \return Number of attributes of node
template<class Ch>
inline std::size_t count_attributes(xml_node<Ch> *node)
{
xml_attribute<Ch> *attr = node->first_attribute();
std::size_t count = 0;
while (attr)
{
++count;
attr = attr->next_attribute();
}
return count;
}
}
#endif

View File

@@ -0,0 +1,425 @@
#include <frostbite2D/resource/audio_database.h>
#include <frostbite2D/resource/asset.h>
#include <rapidxml/rapidxml.hpp>
#include <SDL.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <ctime>
#include <numeric>
#include <random>
namespace frostbite2D {
AudioDatabase& AudioDatabase::get() {
static AudioDatabase instance;
return instance;
}
bool AudioDatabase::loadFromFile(const std::string& path) {
clear();
Asset& asset = Asset::get();
std::string content;
if (!asset.readTextFile(path, content)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 无法读取文件: %s", path.c_str());
return false;
}
return loadFromString(content);
}
bool AudioDatabase::loadFromString(const std::string& xmlContent) {
clear();
if (xmlContent.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: XML 内容为空");
return false;
}
std::string mutableContent = xmlContent;
rapidxml::xml_document<> doc;
try {
doc.parse<rapidxml::parse_default>(&mutableContent[0]);
} catch (const rapidxml::parse_error& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: XML 解析失败: %s", e.what());
return false;
}
rapidxml::xml_node<>* root = doc.first_node("AudioTagDatabase");
if (!root) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 未找到 AudioTagDatabase 根元素");
return false;
}
for (rapidxml::xml_node<>* node = root->first_node(); node; node = node->next_sibling()) {
const char* name = node->name();
if (strcmp(name, "EFFECT") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
if (!idAttr || !fileAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过无效的 EFFECT 条目ID 为空)");
continue;
}
if (strlen(fileAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过 EFFECT 条目FILE 为空): %s", idAttr->value());
continue;
}
AudioEffect effect;
effect.id = idAttr->value();
effect.file = fileAttr->value();
rapidxml::xml_attribute<>* subgroupAttr = node->first_attribute("SUBGROUP");
if (subgroupAttr) {
effect.subgroup = subgroupAttr->value();
}
rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY");
if (loopDelayAttr) {
effect.loopDelay = std::stoi(loopDelayAttr->value());
}
rapidxml::xml_attribute<>* loopDelayRangeAttr = node->first_attribute("LOOP_DELAY_RANGE");
if (loopDelayRangeAttr) {
effect.loopDelayRange = std::stoi(loopDelayRangeAttr->value());
}
effectMap_[effect.id] = std::move(effect);
} else if (strcmp(name, "MUSIC") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
if (!idAttr || !fileAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过无效的 MUSIC 条目ID 为空)");
continue;
}
if (strlen(fileAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过 MUSIC 条目FILE 为空): %s", idAttr->value());
continue;
}
AudioMusic music;
music.id = idAttr->value();
music.file = fileAttr->value();
rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY");
if (loopDelayAttr) {
music.loopDelay = std::stoi(loopDelayAttr->value());
}
musicMap_[music.id] = std::move(music);
} else if (strcmp(name, "RANDOM") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
if (!idAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过无效的 RANDOM 条目ID 为空)");
continue;
}
AudioRandom random;
random.id = idAttr->value();
for (rapidxml::xml_node<>* itemNode = node->first_node("ITEM"); itemNode; itemNode = itemNode->next_sibling("ITEM")) {
rapidxml::xml_attribute<>* tagAttr = itemNode->first_attribute("TAG");
rapidxml::xml_attribute<>* probAttr = itemNode->first_attribute("PROB");
if (!tagAttr || !probAttr) {
continue;
}
if (strlen(tagAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过 RANDOM 条目下的无效 ITEMTAG 为空): %s", random.id.c_str());
continue;
}
RandomItem ri;
ri.tag = tagAttr->value();
ri.probability = std::stoi(probAttr->value());
random.items.push_back(std::move(ri));
}
randomMap_[random.id] = std::move(random);
}
}
loaded_ = true;
SDL_Log("AudioDatabase: 加载完成 - %zu 个音效, %zu 个音乐, %zu 个随机组",
effectMap_.size(), musicMap_.size(), randomMap_.size());
return true;
}
void AudioDatabase::clear() {
effectMap_.clear();
musicMap_.clear();
randomMap_.clear();
loaded_ = false;
}
bool AudioDatabase::isLoaded() const {
return loaded_;
}
std::optional<AudioEntry> AudioDatabase::get(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
AudioEntry entry;
auto effectIt = effectMap_.find(id);
if (effectIt != effectMap_.end()) {
entry.type = AudioEntryType::Effect;
entry.effect = &effectIt->second;
return entry;
}
auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) {
entry.type = AudioEntryType::Music;
entry.music = &musicIt->second;
return entry;
}
auto randomIt = randomMap_.find(id);
if (randomIt != randomMap_.end()) {
entry.type = AudioEntryType::Random;
entry.random = &randomIt->second;
return entry;
}
return std::nullopt;
}
std::optional<std::string> AudioDatabase::getSound(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto randomIt = randomMap_.find(id);
if (randomIt != randomMap_.end()) {
return selectFromRandom(randomIt->second);
}
if (effectMap_.find(id) != effectMap_.end()) {
return id;
}
return std::nullopt;
}
std::optional<std::string> AudioDatabase::getFilePath(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto soundIdOpt = getSound(id);
if (!soundIdOpt) {
auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) {
return musicIt->second.file;
}
return std::nullopt;
}
const std::string& soundId = *soundIdOpt;
auto effectIt = effectMap_.find(soundId);
if (effectIt != effectMap_.end()) {
return effectIt->second.file;
}
return std::nullopt;
}
std::optional<const AudioEffect*> AudioDatabase::getEffect(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = effectMap_.find(id);
if (it != effectMap_.end()) {
return &it->second;
}
return std::nullopt;
}
std::optional<const AudioMusic*> AudioDatabase::getMusic(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = musicMap_.find(id);
if (it != musicMap_.end()) {
return &it->second;
}
return std::nullopt;
}
std::optional<const AudioRandom*> AudioDatabase::getRandom(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = randomMap_.find(id);
if (it != randomMap_.end()) {
return &it->second;
}
return std::nullopt;
}
size_t AudioDatabase::effectCount() const {
return effectMap_.size();
}
size_t AudioDatabase::musicCount() const {
return musicMap_.size();
}
size_t AudioDatabase::randomCount() const {
return randomMap_.size();
}
void AudioDatabase::initializeRNG() {
if (rngInitialized_) {
return;
}
std::random_device rd;
if (rd.entropy() > 0) {
rng_.seed(rd());
} else {
auto now = std::chrono::high_resolution_clock::now();
auto duration = now.time_since_epoch();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
rng_.seed(static_cast<unsigned int>(millis));
}
rngInitialized_ = true;
SDL_Log("AudioDatabase: 随机数生成器已初始化");
}
std::string AudioDatabase::selectFromRandom(const AudioRandom& randomGroup) {
initializeRNG();
std::vector<const RandomItem*> validItems;
for (const auto& item : randomGroup.items) {
if (!item.tag.empty()) {
validItems.push_back(&item);
}
}
if (validItems.empty()) {
return "";
}
if (validItems.size() == 1) {
return validItems[0]->tag;
}
int32 totalWeight = 0;
for (const auto* item : validItems) {
totalWeight += item->probability;
}
if (totalWeight <= 0) {
std::uniform_int_distribution<size_t> dist(0, validItems.size() - 1);
return validItems[dist(rng_)]->tag;
}
std::uniform_int_distribution<int32> dist(1, totalWeight);
int32 randomValue = dist(rng_);
int32 currentWeight = 0;
for (const auto* item : validItems) {
currentWeight += item->probability;
if (randomValue <= currentWeight) {
return item->tag;
}
}
return validItems.back()->tag;
}
bool AudioDatabase::has(const std::string& id) {
if (!loaded_) {
return false;
}
return effectMap_.count(id) > 0 || musicMap_.count(id) > 0 || randomMap_.count(id) > 0;
}
std::string AudioDatabase::filePath(const std::string& id) {
auto opt = getFilePath(id);
return opt ? *opt : "";
}
std::string AudioDatabase::soundId(const std::string& idOrRandomId) {
auto opt = getSound(idOrRandomId);
return opt ? *opt : "";
}
std::string AudioDatabase::subgroup(const std::string& id) {
if (!loaded_) {
return "";
}
auto it = effectMap_.find(id);
if (it != effectMap_.end()) {
return it->second.subgroup;
}
return "";
}
AudioEntryType AudioDatabase::typeOf(const std::string& id) {
if (!loaded_) {
return AudioEntryType::None;
}
if (effectMap_.count(id) > 0) {
return AudioEntryType::Effect;
}
if (musicMap_.count(id) > 0) {
return AudioEntryType::Music;
}
if (randomMap_.count(id) > 0) {
return AudioEntryType::Random;
}
return AudioEntryType::None;
}
bool AudioDatabase::tryGetFilePath(const std::string& id, std::string& outPath) {
auto opt = getFilePath(id);
if (opt) {
outPath = *opt;
return true;
}
return false;
}
bool AudioDatabase::tryGetSoundId(const std::string& id, std::string& outSoundId) {
auto opt = getSound(id);
if (opt) {
outSoundId = *opt;
return true;
}
return false;
}
} // namespace frostbite2D

19186
Game/assets/audio.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
#include <frostbite2D/audio/sound.h> #include <frostbite2D/audio/sound.h>
#include <frostbite2D/resource/npk_archive.h> #include <frostbite2D/resource/npk_archive.h>
#include <frostbite2D/resource/audio_database.h>
#include <frostbite2D/resource/sound_pack_archive.h> #include <frostbite2D/resource/sound_pack_archive.h>
using namespace frostbite2D; using namespace frostbite2D;
@@ -143,6 +144,16 @@ int main(int argc, char **argv) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load music!"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load music!");
} }
auto &audioDB = AudioDatabase::get();
audioDB.loadFromFile("assets/audio.xml");
// ========== 方式 1: 最简单的 API推荐 ==========
std::string path = audioDB.filePath("P_ICECANNON_SHOT");
if (!path.empty()) {
SDL_Log("=== 方式 1: 直接获取文件路径 ===");
SDL_Log("File: %s", path.c_str());
}
app.run(); app.run();
app.shutdown(); app.shutdown();