Merge pull request #99 from skiff/main

[T6] - SndBank Linking and Sound Bank (.sabs/.sabl) Writing
This commit is contained in:
Jan 2024-02-11 11:45:12 +01:00 committed by GitHub
commit b741fe3604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1667 additions and 163 deletions

View File

@ -1,10 +1,16 @@
#include "Crypto.h" #include "Crypto.h"
#include "Impl/AlgorithmMD5.h"
#include "Impl/AlgorithmRSA.h" #include "Impl/AlgorithmRSA.h"
#include "Impl/AlgorithmSHA1.h" #include "Impl/AlgorithmSHA1.h"
#include "Impl/AlgorithmSHA256.h" #include "Impl/AlgorithmSHA256.h"
#include "Impl/AlgorithmSalsa20.h" #include "Impl/AlgorithmSalsa20.h"
std::unique_ptr<IHashFunction> Crypto::CreateMD5()
{
return std::make_unique<AlgorithmMD5>();
}
std::unique_ptr<IHashFunction> Crypto::CreateSHA1() std::unique_ptr<IHashFunction> Crypto::CreateSHA1()
{ {
return std::make_unique<AlgorithmSHA1>(); return std::make_unique<AlgorithmSHA1>();

View File

@ -16,6 +16,8 @@ public:
RSA_PADDING_PSS, RSA_PADDING_PSS,
}; };
static std::unique_ptr<IHashFunction> CreateMD5();
static std::unique_ptr<IHashFunction> CreateSHA1(); static std::unique_ptr<IHashFunction> CreateSHA1();
static std::unique_ptr<IHashFunction> CreateSHA256(); static std::unique_ptr<IHashFunction> CreateSHA256();

View File

@ -0,0 +1,64 @@
#include "AlgorithmMD5.h"
#include "CryptoLibrary.h"
#include <cstdint>
class AlgorithmMD5::AlgorithmMD5Impl
{
hash_state m_state{};
public:
AlgorithmMD5Impl()
{
CryptoLibrary::Init();
Init();
}
void Init()
{
md5_init(&m_state);
}
void Process(const void* input, const size_t inputSize)
{
md5_process(&m_state, static_cast<const uint8_t*>(input), inputSize);
}
void Finish(void* hashBuffer)
{
md5_done(&m_state, static_cast<uint8_t*>(hashBuffer));
}
};
AlgorithmMD5::AlgorithmMD5()
{
m_impl = new AlgorithmMD5Impl();
}
AlgorithmMD5::~AlgorithmMD5()
{
delete m_impl;
m_impl = nullptr;
}
size_t AlgorithmMD5::GetHashSize()
{
return HASH_SIZE;
}
void AlgorithmMD5::Init()
{
m_impl->Init();
}
void AlgorithmMD5::Process(const void* input, const size_t inputSize)
{
m_impl->Process(input, inputSize);
}
void AlgorithmMD5::Finish(void* hashBuffer)
{
m_impl->Finish(hashBuffer);
}

View File

@ -0,0 +1,20 @@
#pragma once
#include "IHashFunction.h"
class AlgorithmMD5 : public IHashFunction
{
class AlgorithmMD5Impl;
AlgorithmMD5Impl* m_impl;
public:
static const int HASH_SIZE = 16;
AlgorithmMD5();
~AlgorithmMD5() override;
size_t GetHashSize() override;
void Init() override;
void Process(const void* input, size_t inputSize) override;
void Finish(void* hashBuffer) override;
};

View File

@ -9,6 +9,7 @@
#include "LinkerSearchPaths.h" #include "LinkerSearchPaths.h"
#include "ObjContainer/IPak/IPakWriter.h" #include "ObjContainer/IPak/IPakWriter.h"
#include "ObjContainer/IWD/IWD.h" #include "ObjContainer/IWD/IWD.h"
#include "ObjContainer/SoundBank/SoundBankWriter.h"
#include "ObjLoading.h" #include "ObjLoading.h"
#include "ObjWriting.h" #include "ObjWriting.h"
#include "SearchPath/SearchPaths.h" #include "SearchPath/SearchPaths.h"
@ -419,6 +420,8 @@ class LinkerImpl final : public Linker
SearchPaths& gdtSearchPaths, SearchPaths& gdtSearchPaths,
SearchPaths& sourceSearchPaths) const SearchPaths& sourceSearchPaths) const
{ {
SoundBankWriter::OutputPath = fs::path(m_args.GetOutputFolderPathForProject(projectName));
const auto zone = CreateZoneForDefinition(targetName, zoneDefinition, &assetSearchPaths, &gdtSearchPaths, &sourceSearchPaths); const auto zone = CreateZoneForDefinition(targetName, zoneDefinition, &assetSearchPaths, &gdtSearchPaths, &sourceSearchPaths);
auto result = zone != nullptr; auto result = zone != nullptr;
if (zone) if (zone)

View File

@ -0,0 +1,80 @@
#include "Csv/ParsedCsv.h"
ParsedCsvRow::ParsedCsvRow(std::unordered_map<std::string, size_t>& headers, std::vector<std::string> row)
: headers(headers),
values(std::move(row))
{
}
std::string ParsedCsvRow::GetValue(const std::string& header, const bool required) const
{
if (this->headers.find(header) == this->headers.end())
{
if (required)
std::cerr << "ERROR: Required column \"" << header << "\" was not found" << std::endl;
else
std::cerr << "WARNING: Expected column \"" << header << "\" was not found" << std::endl;
return {};
}
auto& value = this->values.at(this->headers[header]);
if (required && value.empty())
{
std::cerr << "ERROR: Required column \"" << header << "\" does not have a value" << std::endl;
return {};
}
return value;
}
float ParsedCsvRow::GetValueFloat(const std::string& header, const bool required) const
{
const auto& value = this->GetValue(header, required);
if (!value.empty())
{
std::istringstream ss(value);
float out;
ss >> out;
return out;
}
return {};
}
ParsedCsv::ParsedCsv(const CsvInputStream& inputStream, const bool hasHeaders)
{
std::vector<std::vector<std::string>> csvLines;
std::vector<std::string> currentLine;
while (inputStream.NextRow(currentLine))
{
csvLines.emplace_back(std::move(currentLine));
currentLine = std::vector<std::string>();
}
if (hasHeaders)
{
const auto& headersRow = csvLines[0];
for (auto i = 0u; i < headersRow.size(); i++)
{
this->headers[headersRow[i]] = i;
}
}
for (auto i = hasHeaders ? 1u : 0u; i < csvLines.size(); i++)
{
auto& rowValues = csvLines[i];
this->rows.emplace_back(this->headers, std::move(rowValues));
}
}
size_t ParsedCsv::Size() const
{
return this->rows.size();
}
ParsedCsvRow ParsedCsv::operator[](const size_t index) const
{
return this->rows.at(index);
}

View File

@ -0,0 +1,45 @@
#pragma once
#include "Csv/CsvStream.h"
#include "Utils/ClassUtils.h"
#include <sstream>
#include <unordered_map>
class ParsedCsvRow
{
std::unordered_map<std::string, size_t>& headers;
std::vector<std::string> values;
public:
explicit ParsedCsvRow(std::unordered_map<std::string, size_t>& headers, std::vector<std::string> row);
_NODISCARD std::string GetValue(const std::string& header, bool required = false) const;
_NODISCARD float GetValueFloat(const std::string& header, bool required = false) const;
template<typename T> T GetValueInt(const std::string& header, const bool required = false) const
{
const auto& value = this->GetValue(header, required);
if (!value.empty())
{
std::istringstream ss(value);
long long out;
ss >> out;
return static_cast<T>(out);
}
return {};
}
};
class ParsedCsv
{
std::unordered_map<std::string, size_t> headers;
std::vector<ParsedCsvRow> rows;
public:
explicit ParsedCsv(const CsvInputStream& inputStream, bool hasHeaders = true);
_NODISCARD size_t Size() const;
ParsedCsvRow operator[](size_t index) const;
};

View File

@ -0,0 +1,137 @@
#pragma once
#include <string>
namespace T6
{
inline const std::string SOUND_GROUPS[]{
"grp_reference",
"grp_master",
"grp_wpn_lfe",
"grp_lfe",
"grp_hdrfx",
"grp_music",
"grp_voice",
"grp_set_piece",
"grp_igc",
"grp_mp_game",
"grp_explosion",
"grp_player_impacts",
"grp_scripted_moment",
"grp_menu",
"grp_whizby",
"grp_weapon",
"grp_vehicle",
"grp_impacts",
"grp_foley",
"grp_destructible",
"grp_physics",
"grp_ambience",
"grp_alerts",
"grp_air",
"grp_bink",
"grp_announcer",
"",
};
inline const std::string SOUND_CURVES[]{
"default",
"defaultmin",
"allon",
"alloff",
"rcurve0",
"rcurve1",
"rcurve2",
"rcurve3",
"rcurve4",
"rcurve5",
"steep",
"sindelay",
"cosdelay",
"sin",
"cos",
"rev60",
"rev65",
"",
};
inline const std::string SOUND_DUCK_GROUPS[]{
"snp_alerts_gameplay",
"snp_ambience",
"snp_claw",
"snp_destructible",
"snp_dying",
"snp_dying_ice",
"snp_evt_2d",
"snp_explosion",
"snp_foley",
"snp_grenade",
"snp_hdrfx",
"snp_igc",
"snp_impacts",
"snp_menu",
"snp_movie",
"snp_music",
"snp_never_duck",
"snp_player_dead",
"snp_player_impacts",
"snp_scripted_moment",
"snp_set_piece",
"snp_special",
"snp_vehicle",
"snp_vehicle_interior",
"snp_voice",
"snp_weapon_decay_1p",
"snp_whizby",
"snp_wpn_1p",
"snp_wpn_3p",
"snp_wpn_turret",
"snp_x2",
"snp_x3",
};
inline const std::string SOUND_LIMIT_TYPES[]{
"none",
"oldest",
"reject",
"priority",
};
inline const std::string SOUND_MOVE_TYPES[]{
"none",
"left_player",
"center_player",
"right_player",
"random",
"left_shot",
"center_shot",
"right_shot",
};
inline const std::string SOUND_LOAD_TYPES[]{
"unknown",
"loaded",
"streamed",
"primed",
};
inline const std::string SOUND_BUS_IDS[]{
"bus_reverb",
"bus_fx",
"bus_voice",
"bus_pfutz",
"bus_hdrfx",
"bus_ui",
"bus_reference",
"bus_music",
"bus_movie",
"bus_reference",
"",
};
inline const std::string SOUND_RANDOMIZE_TYPES[]{
"volume",
"pitch",
"variant",
"",
};
} // namespace T6

View File

@ -0,0 +1,246 @@
#include "FlacDecoder.h"
#include "Utils/Alignment.h"
#include "Utils/ClassUtils.h"
#include "Utils/Endianness.h"
#include "Utils/FileUtils.h"
#include <cassert>
#include <iostream>
#include <sstream>
namespace
{
constexpr auto FLAC_MAGIC = FileUtils::MakeMagic32('f', 'L', 'a', 'C');
enum class MetaDataBlockType : unsigned
{
STREAMINFO = 0,
PADDING = 1,
APPLICATION = 2,
SEEKTABLE = 3,
VORBIS_COMMENT = 4,
CUESHEET = 5,
PICTURE = 6
};
struct MetaDataBlockHeader
{
uint8_t isLastMetaDataBlock;
MetaDataBlockType blockType;
uint32_t blockLength;
};
constexpr auto STREAM_INFO_BLOCK_SIZE = 34;
class FlacReadingException final : public std::exception
{
public:
explicit FlacReadingException(std::string message)
: m_message(std::move(message))
{
}
_NODISCARD char const* what() const noexcept override
{
return m_message.c_str();
}
private:
std::string m_message;
};
class FlacBitReader
{
public:
explicit FlacBitReader(std::istream& stream)
: m_stream(stream),
m_last_byte(0u),
m_remaining_bits_last_byte(0u)
{
}
template<typename T> T ReadBits(const size_t bitCount)
{
union
{
uint8_t buffer[sizeof(T)];
T result;
} data{};
const auto byteCount = utils::Align(bitCount, 8u) / 8u;
assert(byteCount <= sizeof(T));
const auto shiftCount = (8u - bitCount % 8) % 8;
auto remainingBits = bitCount;
#if HOST_ENDIANNESS == LITTLE_ENDIAN_ENDIANNESS
auto offset = byteCount - 1;
#else
auto offset = 0u;
#endif
while (remainingBits > 0)
{
const auto curBits = static_cast<uint8_t>(std::min(remainingBits, 8u));
if (m_remaining_bits_last_byte > 0)
{
if (m_remaining_bits_last_byte < curBits)
{
const auto bitsFromFirstByte = m_remaining_bits_last_byte;
data.buffer[offset] = static_cast<uint8_t>(m_last_byte << (8u - bitsFromFirstByte));
m_stream.read(reinterpret_cast<char*>(&m_last_byte), sizeof(m_last_byte));
if (m_stream.gcount() != sizeof(m_last_byte))
throw FlacReadingException("Unexpected eof");
const auto bitsFromSecondByte = static_cast<uint8_t>(curBits - m_remaining_bits_last_byte);
m_remaining_bits_last_byte = 8u - bitsFromSecondByte;
const auto maskForSecondByte = static_cast<uint8_t>(0xFF << (8u - bitsFromSecondByte));
data.buffer[offset] |= (m_last_byte & maskForSecondByte) >> bitsFromFirstByte;
}
else if (m_remaining_bits_last_byte == curBits)
{
data.buffer[offset] = static_cast<uint8_t>(m_last_byte << (8u - curBits));
m_remaining_bits_last_byte = 0u;
}
else // m_remaining_bits_last_byte > curBits
{
const auto maskForCurBits = 0xFF >> (8u - curBits);
const auto maskForCurBitsInRemainingBits = static_cast<uint8_t>(maskForCurBits << (m_remaining_bits_last_byte - curBits));
const auto selectedData = static_cast<uint8_t>(m_last_byte & maskForCurBitsInRemainingBits);
data.buffer[offset] = static_cast<uint8_t>(selectedData << (8u - m_remaining_bits_last_byte));
m_remaining_bits_last_byte -= curBits;
}
}
else if (curBits >= 8u)
{
m_stream.read(reinterpret_cast<char*>(&data.buffer[offset]), sizeof(uint8_t));
if (m_stream.gcount() != sizeof(uint8_t))
throw FlacReadingException("Unexpected eof");
}
else
{
m_stream.read(reinterpret_cast<char*>(&m_last_byte), sizeof(m_last_byte));
if (m_stream.gcount() != sizeof(m_last_byte))
throw FlacReadingException("Unexpected eof");
data.buffer[offset] = m_last_byte & (0xFF << (8u - curBits));
m_remaining_bits_last_byte = static_cast<uint8_t>(8u - curBits);
}
remainingBits -= curBits;
#if HOST_ENDIANNESS == LITTLE_ENDIAN_ENDIANNESS
--offset;
#else
++offset;
#endif
}
data.result >>= shiftCount;
return data.result;
}
void ReadBuffer(void* buffer, const size_t bitCount)
{
assert(m_remaining_bits_last_byte == 0);
assert(bitCount % 8 == 0);
m_remaining_bits_last_byte = 0;
m_stream.read(static_cast<char*>(buffer), bitCount / 8);
}
void Seek(const size_t offset)
{
assert(m_remaining_bits_last_byte == 0);
m_remaining_bits_last_byte = 0;
m_stream.seekg(offset, std::ios::cur);
}
private:
std::istream& m_stream;
uint8_t m_last_byte;
uint8_t m_remaining_bits_last_byte;
};
} // namespace
namespace flac
{
FlacMetaData::FlacMetaData()
: m_minimum_block_size(),
m_maximum_block_size(),
m_minimum_frame_size(),
m_maximum_frame_size(),
m_sample_rate(),
m_number_of_channels(),
m_bits_per_sample(),
m_total_samples(),
m_md5_signature{}
{
}
void FlacReadStreamInfo(FlacBitReader& reader, FlacMetaData& metaData)
{
metaData.m_minimum_block_size = reader.ReadBits<uint16_t>(16);
metaData.m_maximum_block_size = reader.ReadBits<uint16_t>(16);
metaData.m_minimum_frame_size = reader.ReadBits<uint32_t>(24);
metaData.m_maximum_frame_size = reader.ReadBits<uint32_t>(24);
metaData.m_sample_rate = reader.ReadBits<uint32_t>(20);
metaData.m_number_of_channels = static_cast<uint8_t>(reader.ReadBits<uint8_t>(3) + 1);
metaData.m_bits_per_sample = static_cast<uint8_t>(reader.ReadBits<uint8_t>(5) + 1);
metaData.m_total_samples = reader.ReadBits<uint64_t>(36);
reader.ReadBuffer(metaData.m_md5_signature, 128);
}
bool GetFlacMetaData(std::istream& stream, FlacMetaData& metaData)
{
try
{
uint32_t readMagic;
stream.read(reinterpret_cast<char*>(&readMagic), sizeof(readMagic));
if (stream.gcount() != sizeof(readMagic) || readMagic != FLAC_MAGIC)
throw FlacReadingException("Invalid flac magic");
FlacBitReader reader(stream);
while (true)
{
const MetaDataBlockHeader header{
reader.ReadBits<uint8_t>(1),
static_cast<MetaDataBlockType>(reader.ReadBits<uint8_t>(7)),
reader.ReadBits<uint32_t>(24),
};
if (header.blockType == MetaDataBlockType::STREAMINFO)
{
if (header.blockLength != STREAM_INFO_BLOCK_SIZE)
throw FlacReadingException("Flac stream info block size invalid");
FlacReadStreamInfo(reader, metaData);
return true;
}
reader.Seek(header.blockLength * 8u);
if (header.isLastMetaDataBlock)
break;
}
throw FlacReadingException("Missing flac stream info block");
}
catch (const FlacReadingException& e)
{
std::cerr << e.what() << "\n";
}
return false;
}
bool GetFlacMetaData(const void* data, const size_t dataSize, FlacMetaData& metaData)
{
std::istringstream ss(std::string(static_cast<const char*>(data), dataSize));
return GetFlacMetaData(ss, metaData);
}
} // namespace flac

View File

@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <istream>
namespace flac
{
class FlacMetaData
{
public:
uint16_t m_minimum_block_size;
uint16_t m_maximum_block_size;
uint32_t m_minimum_frame_size;
uint32_t m_maximum_frame_size;
uint32_t m_sample_rate;
uint8_t m_number_of_channels;
uint8_t m_bits_per_sample;
uint64_t m_total_samples;
uint8_t m_md5_signature[16];
FlacMetaData();
};
bool GetFlacMetaData(std::istream& stream, FlacMetaData& metaData);
bool GetFlacMetaData(const void* data, size_t dataSize, FlacMetaData& metaData);
} // namespace flac

View File

@ -28,3 +28,20 @@ struct WavFormatChunkPcm
uint16_t nBlockAlign; uint16_t nBlockAlign;
uint16_t wBitsPerSample; uint16_t wBitsPerSample;
}; };
struct WavMetaData
{
unsigned channelCount;
unsigned samplesPerSec;
unsigned bitsPerSample;
};
struct WavHeader
{
unsigned int chunkIdRiff;
unsigned int chunkIdSize;
unsigned int format;
WavChunkHeader chunkHeader;
WavFormatChunkPcm formatChunk;
WavChunkHeader subChunkHeader;
};

View File

@ -55,4 +55,5 @@ function ObjLoading:project()
minilzo:include(includes) minilzo:include(includes)
minizip:include(includes) minizip:include(includes)
zlib:include(includes) zlib:include(includes)
json:include(includes)
end end

View File

@ -0,0 +1,575 @@
#include "AssetLoaderSoundBank.h"
#include "Csv/ParsedCsv.h"
#include "Game/T6/CommonT6.h"
#include "Game/T6/SoundConstantsT6.h"
#include "Game/T6/T6.h"
#include "ObjContainer/SoundBank/SoundBankWriter.h"
#include "Pool/GlobalAssetPool.h"
#include "Utils/StringUtils.h"
#include <cstring>
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
using namespace T6;
namespace fs = std::filesystem;
namespace
{
const std::string PREFIXES_TO_DROP[]{
"raw/",
"devraw/",
};
_NODISCARD std::string GetSoundFilePath(const SndAlias* sndAlias)
{
std::string soundFilePath(sndAlias->assetFileName);
std::replace(soundFilePath.begin(), soundFilePath.end(), '\\', '/');
for (const auto& droppedPrefix : PREFIXES_TO_DROP)
{
if (soundFilePath.rfind(droppedPrefix, 0) != std::string::npos)
{
soundFilePath.erase(0, droppedPrefix.size());
break;
}
}
return soundFilePath;
}
_NODISCARD std::unique_ptr<std::ofstream> OpenSoundBankOutputFile(const std::string& bankName)
{
fs::path assetPath = SoundBankWriter::OutputPath / bankName;
auto assetDir(assetPath);
assetDir.remove_filename();
create_directories(assetDir);
auto outputStream = std::make_unique<std::ofstream>(assetPath, std::ios_base::out | std::ios_base::binary);
if (outputStream->is_open())
{
return std::move(outputStream);
}
return nullptr;
}
} // namespace
void* AssetLoaderSoundBank::CreateEmptyAsset(const std::string& assetName, MemoryManager* memory)
{
auto* soundBank = memory->Create<SndBank>();
memset(soundBank, 0, sizeof(SndBank));
soundBank->name = memory->Dup(assetName.c_str());
return soundBank;
}
bool AssetLoaderSoundBank::CanLoadFromRaw() const
{
return true;
}
size_t GetValueIndex(const std::string& value, const std::string* lookupTable, size_t len)
{
if (value.empty())
return 0;
for (auto i = 0u; i < len; i++)
{
if (lookupTable[i] == value)
return i;
}
return 0;
}
unsigned int GetAliasSubListCount(const unsigned int startRow, const ParsedCsv& csv)
{
auto count = 1u;
const auto name = csv[startRow].GetValue("name", true);
if (name.empty())
return 0;
while (true)
{
if (startRow + count >= csv.Size())
break;
const auto testName = csv[startRow + count].GetValue("name", true);
if (testName.empty())
break;
// if the name of the next entry does not match the first entry checked, it is not part of the sub list
if (name != testName)
break;
count++;
}
return count;
}
bool LoadSoundAlias(MemoryManager* memory, SndAlias* alias, const ParsedCsvRow& row)
{
memset(alias, 0, sizeof(SndAlias));
const auto& name = row.GetValue("name", true);
if (name.empty())
return false;
const auto& aliasFileName = row.GetValue("file", true);
if (aliasFileName.empty())
return false;
alias->name = memory->Dup(name.data());
alias->id = Common::SND_HashName(name.data());
alias->assetFileName = memory->Dup(aliasFileName.data());
alias->assetId = Common::SND_HashName(aliasFileName.data());
const auto secondaryName = row.GetValue("secondary");
if (!secondaryName.empty())
alias->secondaryname = memory->Dup(secondaryName.data());
const auto subtitle = row.GetValue("subtitle");
if (!subtitle.empty())
alias->subtitle = memory->Dup(subtitle.data());
alias->duck = Common::SND_HashName(row.GetValue("duck").data());
alias->volMin = row.GetValueInt<uint16_t>("vol_min");
alias->volMax = row.GetValueInt<uint16_t>("vol_max");
alias->distMin = row.GetValueInt<uint16_t>("dist_min");
alias->distMax = row.GetValueInt<uint16_t>("dist_max");
alias->distReverbMax = row.GetValueInt<uint16_t>("dist_reverb_max");
alias->limitCount = row.GetValueInt<uint8_t>("limit_count");
alias->entityLimitCount = row.GetValueInt<uint8_t>("entity_limit_count");
alias->pitchMin = row.GetValueInt<uint16_t>("pitch_min");
alias->pitchMax = row.GetValueInt<uint16_t>("pitch_max");
alias->minPriority = row.GetValueInt<uint8_t>("min_priority");
alias->maxPriority = row.GetValueInt<uint8_t>("max_priority");
alias->minPriorityThreshold = row.GetValueInt<uint8_t>("min_priority_threshold");
alias->maxPriorityThreshold = row.GetValueInt<uint8_t>("max_priority_threshold");
alias->probability = row.GetValueInt<uint8_t>("probability");
alias->startDelay = row.GetValueInt<uint16_t>("start_delay");
alias->reverbSend = row.GetValueInt<uint16_t>("reverb_send");
alias->centerSend = row.GetValueInt<uint16_t>("center_send");
alias->envelopMin = row.GetValueInt<uint16_t>("envelop_min");
alias->envelopMax = row.GetValueInt<uint16_t>("envelop_max");
alias->envelopPercentage = row.GetValueInt<uint16_t>("envelop_percentage");
alias->occlusionLevel = row.GetValueInt<uint8_t>("occlusion_level");
alias->fluxTime = row.GetValueInt<uint16_t>("move_time");
alias->futzPatch = row.GetValueInt<unsigned int>("futz");
alias->contextType = row.GetValueInt<unsigned int>("context_type");
alias->contextValue = row.GetValueInt<unsigned int>("context_value");
alias->fadeIn = row.GetValueInt<int16_t>("fade_in");
alias->fadeOut = row.GetValueInt<int16_t>("fade_out");
alias->flags.looping = row.GetValue("loop") == "looping";
alias->flags.panType = row.GetValue("pan") == "3d";
alias->flags.isBig = row.GetValue("is_big") == "yes";
alias->flags.distanceLpf = row.GetValue("distance_lpf") == "yes";
alias->flags.doppler = row.GetValue("doppler") == "yes";
alias->flags.timescale = row.GetValue("timescale") == "yes";
alias->flags.isMusic = row.GetValue("music") == "yes";
alias->flags.pauseable = row.GetValue("pause") == "yes";
alias->flags.stopOnDeath = row.GetValue("stop_on_death") == "yes";
alias->duckGroup = static_cast<char>(GetValueIndex(row.GetValue("duck_group"), SOUND_DUCK_GROUPS, std::extent_v<decltype(SOUND_DUCK_GROUPS)>));
alias->flags.volumeGroup = GetValueIndex(row.GetValue("group"), SOUND_GROUPS, std::extent_v<decltype(SOUND_GROUPS)>);
alias->flags.fluxType = GetValueIndex(row.GetValue("move_type"), SOUND_MOVE_TYPES, std::extent_v<decltype(SOUND_MOVE_TYPES)>);
alias->flags.loadType = GetValueIndex(row.GetValue("type"), SOUND_LOAD_TYPES, std::extent_v<decltype(SOUND_LOAD_TYPES)>);
alias->flags.busType = GetValueIndex(row.GetValue("bus"), SOUND_BUS_IDS, std::extent_v<decltype(SOUND_BUS_IDS)>);
alias->flags.limitType = GetValueIndex(row.GetValue("limit_type"), SOUND_LIMIT_TYPES, std::extent_v<decltype(SOUND_LIMIT_TYPES)>);
alias->flags.volumeFalloffCurve = GetValueIndex(row.GetValue("volume_falloff_curve"), SOUND_CURVES, std::extent_v<decltype(SOUND_CURVES)>);
alias->flags.reverbFalloffCurve = GetValueIndex(row.GetValue("reverb_falloff_curve"), SOUND_CURVES, std::extent_v<decltype(SOUND_CURVES)>);
alias->flags.entityLimitType = GetValueIndex(row.GetValue("entity_limit_type"), SOUND_LIMIT_TYPES, std::extent_v<decltype(SOUND_LIMIT_TYPES)>);
alias->flags.volumeMinFalloffCurve = GetValueIndex(row.GetValue("volume_min_falloff_curve"), SOUND_CURVES, std::extent_v<decltype(SOUND_CURVES)>);
alias->flags.reverbMinFalloffCurve = GetValueIndex(row.GetValue("reverb_min_falloff_curve"), SOUND_CURVES, std::extent_v<decltype(SOUND_CURVES)>);
alias->flags.randomizeType = GetValueIndex(row.GetValue("randomize_type"), SOUND_RANDOMIZE_TYPES, std::extent_v<decltype(SOUND_RANDOMIZE_TYPES)>);
return true;
}
bool LoadSoundAliasIndexList(MemoryManager* memory, SndBank* sndBank)
{
// contains a list of all the alias ids in the sound bank
sndBank->aliasIndex = static_cast<SndIndexEntry*>(memory->Alloc(sizeof(SndIndexEntry) * sndBank->aliasCount));
memset(sndBank->aliasIndex, 0xFF, sizeof(SndIndexEntry) * sndBank->aliasCount);
const auto setAliasIndexList = std::make_unique<bool[]>(sndBank->aliasCount);
for (auto i = 0u; i < sndBank->aliasCount; i++)
{
const auto idx = sndBank->alias[i].id % sndBank->aliasCount;
if (sndBank->aliasIndex[idx].value == std::numeric_limits<unsigned short>::max())
{
sndBank->aliasIndex[idx].value = i;
sndBank->aliasIndex[idx].next = std::numeric_limits<unsigned short>::max();
setAliasIndexList[i] = true;
}
}
for (auto i = 0u; i < sndBank->aliasCount; i++)
{
if (setAliasIndexList[i])
continue;
auto idx = sndBank->alias[i].id % sndBank->aliasCount;
while (sndBank->aliasIndex[idx].next != std::numeric_limits<unsigned short>::max())
{
idx = sndBank->aliasIndex[idx].next;
}
auto offset = 1u;
auto freeIdx = std::numeric_limits<unsigned short>::max();
while (true)
{
freeIdx = (idx + offset) % sndBank->aliasCount;
if (sndBank->aliasIndex[freeIdx].value == std::numeric_limits<unsigned short>::max())
break;
freeIdx = (idx + sndBank->aliasCount - offset) % sndBank->aliasCount;
if (sndBank->aliasIndex[freeIdx].value == std::numeric_limits<unsigned short>::max())
break;
offset++;
freeIdx = std::numeric_limits<unsigned short>::max();
if (offset >= sndBank->aliasCount)
break;
}
if (freeIdx == std::numeric_limits<unsigned short>::max())
{
std::cerr << "Unable to allocate sound bank alias index list" << std::endl;
return false;
}
sndBank->aliasIndex[idx].next = freeIdx;
sndBank->aliasIndex[freeIdx].value = i;
sndBank->aliasIndex[freeIdx].next = std::numeric_limits<unsigned short>::max();
setAliasIndexList[i] = true;
}
return true;
}
bool LoadSoundAliasList(
MemoryManager* memory, SndBank* sndBank, const SearchPathOpenFile& file, unsigned int* loadedEntryCount, unsigned int* streamedEntryCount)
{
const CsvInputStream aliasCsvStream(*file.m_stream);
const ParsedCsv aliasCsv(aliasCsvStream, true);
// Ensure there is at least one entry in the csv after the headers
if (aliasCsv.Size() > 0)
{
// should be the total number of assets
sndBank->aliasCount = aliasCsv.Size();
sndBank->alias = static_cast<SndAliasList*>(memory->Alloc(sizeof(SndAliasList) * sndBank->aliasCount));
memset(sndBank->alias, 0, sizeof(SndAliasList) * sndBank->aliasCount);
auto row = 0u;
auto listIndex = 0u;
while (row < sndBank->aliasCount)
{
// count how many of the next rows should be in the sound alias sub-list. Aliases are part of the same sub list if they have the same name for a
// different file
const auto subListCount = GetAliasSubListCount(row, aliasCsv);
if (subListCount < 1)
return false;
// allocate the sub list
sndBank->alias[listIndex].count = subListCount;
sndBank->alias[listIndex].head = static_cast<SndAlias*>(memory->Alloc(sizeof(SndAlias) * subListCount));
sndBank->alias[listIndex].sequence = 0;
// populate the sublist with the next X number of aliases in the file. Note: this will only work correctly if the aliases that are a part of a sub
// list are next to each other in the file
for (auto i = 0u; i < subListCount; i++)
{
if (!LoadSoundAlias(memory, &sndBank->alias[listIndex].head[i], aliasCsv[row]))
return false;
// if this asset is loaded instead of stream, increment the loaded count for later
if (sndBank->alias[listIndex].head[i].flags.loadType == SA_LOADED)
(*loadedEntryCount)++;
else
(*streamedEntryCount)++;
row++;
}
// the main alias list id and name should match that of the entries in the sub list (since they all have the same name, all sub entries will be the
// same)
sndBank->alias[listIndex].id = sndBank->alias[listIndex].head[0].id;
sndBank->alias[listIndex].name = sndBank->alias[listIndex].head[0].name;
listIndex++;
}
// re-allocate the alias list and count if necessary. We don't know the true aliasCount until after parsing all the aliases in the file
if (listIndex != sndBank->aliasCount)
{
auto* oldAliases = sndBank->alias;
sndBank->aliasCount = listIndex;
sndBank->alias = static_cast<SndAliasList*>(memory->Alloc(sizeof(SndAliasList) * sndBank->aliasCount));
memcpy(sndBank->alias, oldAliases, sizeof(SndAliasList) * sndBank->aliasCount);
memory->Free(oldAliases);
}
if (!LoadSoundAliasIndexList(memory, sndBank))
return false;
}
return true;
}
bool LoadSoundRadverbs(MemoryManager* memory, SndBank* sndBank, const SearchPathOpenFile& file)
{
const CsvInputStream radverbCsvStream(*file.m_stream);
const ParsedCsv radverbCsv(radverbCsvStream, true);
if (radverbCsv.Size() > 0)
{
sndBank->radverbCount = radverbCsv.Size();
sndBank->radverbs = static_cast<SndRadverb*>(memory->Alloc(sizeof(SndRadverb) * sndBank->radverbCount));
memset(sndBank->radverbs, 0, sizeof(SndRadverb) * sndBank->radverbCount);
for (auto i = 0u; i < sndBank->radverbCount; i++)
{
auto row = radverbCsv[i];
auto name = row.GetValue("name", true);
if (name.empty())
return false;
strncpy(sndBank->radverbs[i].name, name.data(), 32);
sndBank->radverbs[i].id = Common::SND_HashName(name.data());
sndBank->radverbs[i].smoothing = row.GetValueFloat("smoothing");
sndBank->radverbs[i].earlyTime = row.GetValueFloat("earlyTime");
sndBank->radverbs[i].lateTime = row.GetValueFloat("lateTime");
sndBank->radverbs[i].earlyGain = row.GetValueFloat("earlyGain");
sndBank->radverbs[i].lateGain = row.GetValueFloat("lateGain");
sndBank->radverbs[i].returnGain = row.GetValueFloat("returnGain");
sndBank->radverbs[i].earlyLpf = row.GetValueFloat("earlyLpf");
sndBank->radverbs[i].lateLpf = row.GetValueFloat("lateLpf");
sndBank->radverbs[i].inputLpf = row.GetValueFloat("inputLpf");
sndBank->radverbs[i].dampLpf = row.GetValueFloat("dampLpf");
sndBank->radverbs[i].wallReflect = row.GetValueFloat("wallReflect");
sndBank->radverbs[i].dryGain = row.GetValueFloat("dryGain");
sndBank->radverbs[i].earlySize = row.GetValueFloat("earlySize");
sndBank->radverbs[i].lateSize = row.GetValueFloat("lateSize");
sndBank->radverbs[i].diffusion = row.GetValueFloat("diffusion");
sndBank->radverbs[i].returnHighpass = row.GetValueFloat("returnHighpass");
}
}
return true;
}
bool LoadSoundDuckList(ISearchPath* searchPath, MemoryManager* memory, SndBank* sndBank, const SearchPathOpenFile& file)
{
const CsvInputStream duckListCsvStream(*file.m_stream);
const ParsedCsv duckListCsv(duckListCsvStream, true);
if (duckListCsv.Size() > 0)
{
sndBank->duckCount = duckListCsv.Size();
sndBank->ducks = static_cast<SndDuck*>(memory->Alloc(sizeof(SndDuck) * sndBank->duckCount));
memset(sndBank->ducks, 0, sizeof(SndDuck) * sndBank->duckCount);
for (auto i = 0u; i < sndBank->duckCount; i++)
{
auto row = duckListCsv[i];
const auto name = row.GetValue("name", true);
if (name.empty())
return false;
const auto duckFile = searchPath->Open("soundbank/ducks/" + name + ".duk");
if (!duckFile.IsOpen())
{
std::cerr << "Unable to find .duk file for " << name << " in ducklist for sound bank " << sndBank->name << std::endl;
return false;
}
auto* duck = &sndBank->ducks[i];
strncpy(duck->name, name.data(), 32);
duck->id = Common::SND_HashName(name.data());
auto duckJson = nlohmann::json::parse(*duckFile.m_stream);
duck->fadeIn = duckJson["fadeIn"].get<float>();
duck->fadeOut = duckJson["fadeOut"].get<float>();
duck->startDelay = duckJson["startDelay"].get<float>();
duck->distance = duckJson["distance"].get<float>();
duck->length = duckJson["length"].get<float>();
duck->updateWhilePaused = duckJson["updateWhilePaused"].get<int>();
duck->fadeInCurve = duckJson["fadeInCurveId"].get<unsigned int>();
duck->fadeOutCurve = duckJson["fadeOutCurveId"].get<unsigned int>();
if (duckJson.contains("fadeInCurve"))
duck->fadeInCurve = Common::SND_HashName(duckJson["fadeInCurve"].get<std::string>().data());
if (duckJson.contains("fadeOutCurve"))
duck->fadeOutCurve = Common::SND_HashName(duckJson["fadeOutCurve"].get<std::string>().data());
duck->attenuation = static_cast<SndFloatAlign16*>(memory->Alloc(sizeof(SndFloatAlign16) * 32));
duck->filter = static_cast<SndFloatAlign16*>(memory->Alloc(sizeof(SndFloatAlign16) * 32));
for (auto& valueJson : duckJson["values"])
{
auto index = GetValueIndex(valueJson["duckGroup"].get<std::string>(), SOUND_DUCK_GROUPS, std::extent_v<decltype(SOUND_DUCK_GROUPS)>);
duck->attenuation[index] = valueJson["attenuation"].get<float>();
duck->filter[index] = valueJson["filter"].get<float>();
}
}
}
return true;
}
bool AssetLoaderSoundBank::LoadFromRaw(
const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const
{
if (assetName.find('.') == std::string::npos)
{
std::cerr << "A language must be specific in the soundbank asset name! (Ex: mpl_common.all)" << std::endl;
return false;
}
// open the soundbank aliases
const auto aliasFile = searchPath->Open("soundbank/" + assetName + ".aliases.csv");
if (!aliasFile.IsOpen())
return false;
// set the defaults
auto* sndBank = memory->Create<SndBank>();
memset(sndBank, 0, sizeof(SndBank));
sndBank->name = memory->Dup(assetName.c_str());
const auto sndBankLocalization = utils::StringSplit(assetName, '.');
// load the soundbank aliases
unsigned int loadedEntryCount = 0u, streamedEntryCount = 0u;
if (!LoadSoundAliasList(memory, sndBank, aliasFile, &loadedEntryCount, &streamedEntryCount))
return false;
// load the soundbank reverbs
const auto radverbFile = searchPath->Open("soundbank/" + assetName + ".reverbs.csv");
if (radverbFile.IsOpen())
{
if (!LoadSoundRadverbs(memory, sndBank, radverbFile))
{
std::cerr << "Sound Bank reverbs file for " << assetName << " is invalid" << std::endl;
return false;
}
}
// load the soundbank ducks
const auto duckListFile = searchPath->Open("soundbank/" + assetName + ".ducklist.csv");
if (duckListFile.IsOpen())
{
if (!LoadSoundDuckList(searchPath, memory, sndBank, duckListFile))
{
std::cerr << "Sound Bank ducklist file for " << assetName << " is invalid" << std::endl;
return false;
}
}
std::unique_ptr<std::ofstream> sablStream, sabsStream;
std::unique_ptr<SoundBankWriter> sablWriter, sabsWriter;
if (loadedEntryCount > 0)
{
sndBank->loadAssetBank.zone = memory->Dup(sndBankLocalization.at(0).c_str());
sndBank->loadAssetBank.language = memory->Dup(sndBankLocalization.at(1).c_str());
memset(sndBank->loadAssetBank.linkTimeChecksum, 0xCC, 16);
sndBank->loadedAssets.loadedCount = 0;
sndBank->loadedAssets.zone = memory->Dup(sndBankLocalization.at(0).c_str());
sndBank->loadedAssets.language = memory->Dup(sndBankLocalization.at(1).c_str());
sndBank->loadedAssets.entryCount = loadedEntryCount;
sndBank->loadedAssets.entries = static_cast<SndAssetBankEntry*>(memory->Alloc(sizeof(SndAssetBankEntry) * loadedEntryCount));
memset(sndBank->loadedAssets.entries, 0, sizeof(SndAssetBankEntry) * loadedEntryCount);
sndBank->runtimeAssetLoad = true;
const auto sablName = assetName + ".sabl";
sablStream = OpenSoundBankOutputFile(sablName);
if (sablStream)
sablWriter = SoundBankWriter::Create(sablName, *sablStream, searchPath);
}
if (streamedEntryCount > 0)
{
sndBank->streamAssetBank.zone = memory->Dup(sndBankLocalization.at(0).c_str());
sndBank->streamAssetBank.language = memory->Dup(sndBankLocalization.at(1).c_str());
memset(sndBank->streamAssetBank.linkTimeChecksum, 0xCC, 16);
const auto sabsName = assetName + ".sabs";
sabsStream = OpenSoundBankOutputFile(sabsName);
if (sabsStream)
sabsWriter = SoundBankWriter::Create(sabsName, *sabsStream, searchPath);
}
// add aliases to the correct sound bank writer
for (auto i = 0u; i < sndBank->aliasCount; i++)
{
const auto* aliasList = &sndBank->alias[i];
for (auto j = 0; j < aliasList->count; j++)
{
const auto* alias = &aliasList->head[j];
if (sabsWriter && alias->flags.loadType == SA_STREAMED)
sabsWriter->AddSound(GetSoundFilePath(alias), alias->assetId, alias->flags.looping, true);
else if (sablWriter)
sablWriter->AddSound(GetSoundFilePath(alias), alias->assetId, alias->flags.looping);
}
}
// write the output linked sound bank
if (sablWriter)
{
size_t dataSize = 0u;
const auto result = sablWriter->Write(dataSize);
sablStream->close();
if (result)
{
sndBank->loadedAssets.dataSize = dataSize;
sndBank->loadedAssets.data = static_cast<SndChar2048*>(memory->Alloc(dataSize));
memset(sndBank->loadedAssets.data, 0, dataSize);
}
else
{
std::cerr << "Loaded Sound Bank for " << assetName << " failed to generate. Please check your build files." << std::endl;
return false;
}
}
// write the output streamed sound bank
if (sabsWriter)
{
size_t dataSize = 0u;
const auto result = sabsWriter->Write(dataSize);
sabsStream->close();
if (!result)
{
std::cerr << "Streamed Sound Bank for " << assetName << " failed to generate. Please check your build files." << std::endl;
return false;
}
}
manager->AddAsset(ASSET_TYPE_SOUND, assetName, sndBank);
return true;
}

View File

@ -0,0 +1,17 @@
#pragma once
#include "AssetLoading/BasicAssetLoader.h"
#include "AssetLoading/IAssetLoadingManager.h"
#include "Game/T6/T6.h"
#include "SearchPath/ISearchPath.h"
namespace T6
{
class AssetLoaderSoundBank final : public BasicAssetLoader<ASSET_TYPE_SOUND, SndBank>
{
public:
_NODISCARD void* CreateEmptyAsset(const std::string& assetName, MemoryManager* memory) override;
_NODISCARD bool CanLoadFromRaw() const override;
bool
LoadFromRaw(const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const override;
};
} // namespace T6

View File

@ -9,6 +9,7 @@
#include "AssetLoaders/AssetLoaderRawFile.h" #include "AssetLoaders/AssetLoaderRawFile.h"
#include "AssetLoaders/AssetLoaderScriptParseTree.h" #include "AssetLoaders/AssetLoaderScriptParseTree.h"
#include "AssetLoaders/AssetLoaderSlug.h" #include "AssetLoaders/AssetLoaderSlug.h"
#include "AssetLoaders/AssetLoaderSoundBank.h"
#include "AssetLoaders/AssetLoaderStringTable.h" #include "AssetLoaders/AssetLoaderStringTable.h"
#include "AssetLoaders/AssetLoaderTracer.h" #include "AssetLoaders/AssetLoaderTracer.h"
#include "AssetLoaders/AssetLoaderVehicle.h" #include "AssetLoaders/AssetLoaderVehicle.h"
@ -51,7 +52,7 @@ namespace T6
REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_MATERIAL, Material)) REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_MATERIAL, Material))
REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_TECHNIQUE_SET, MaterialTechniqueSet)) REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_TECHNIQUE_SET, MaterialTechniqueSet))
REGISTER_ASSET_LOADER(AssetLoaderGfxImage) REGISTER_ASSET_LOADER(AssetLoaderGfxImage)
REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_SOUND, SndBank)) REGISTER_ASSET_LOADER(AssetLoaderSoundBank)
REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_SOUND_PATCH, SndPatch)) REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_SOUND_PATCH, SndPatch))
REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_CLIPMAP, clipMap_t)) REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_CLIPMAP, clipMap_t))
REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_CLIPMAP_PVS, clipMap_t)) REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_CLIPMAP_PVS, clipMap_t))

View File

@ -0,0 +1,314 @@
#include "SoundBankWriter.h"
#include "Crypto.h"
#include "ObjContainer/SoundBank/SoundBankTypes.h"
#include "Sound/FlacDecoder.h"
#include "Sound/WavTypes.h"
#include "Utils/FileUtils.h"
#include <cstring>
#include <filesystem>
#include <iostream>
#include <unordered_map>
std::unordered_map<unsigned int, unsigned char> INDEX_FOR_FRAMERATE{
{8000, 0},
{12000, 1},
{16000, 2},
{24000, 3},
{32000, 4},
{44100, 5},
{48000, 6},
{96000, 7},
{192000, 8},
};
class SoundBankWriterImpl : public SoundBankWriter
{
static constexpr char BRANDING[] = "Created with OAT - OpenAssetTools";
static constexpr int64_t DATA_OFFSET = 0x800;
static constexpr uint32_t MAGIC = FileUtils::MakeMagic32('2', 'U', 'X', '#');
static constexpr uint32_t VERSION = 14u;
inline static const std::string PAD_DATA = std::string(16, '\x00');
public:
explicit SoundBankWriterImpl(std::string fileName, std::ostream& stream, ISearchPath* assetSearchPath)
: m_file_name(std::move(fileName)),
m_stream(stream),
m_asset_search_path(assetSearchPath),
m_current_offset(0),
m_total_size(0),
m_entry_section_offset(0),
m_checksum_section_offset(0)
{
}
void AddSound(const std::string& soundFilePath, unsigned int soundId, bool looping, bool streamed) override
{
this->m_sounds.emplace_back(soundFilePath, soundId, looping, streamed);
}
void GoTo(const int64_t offset)
{
m_stream.seekp(offset, std::ios::beg);
m_current_offset = offset;
}
void Write(const void* data, const size_t dataSize)
{
m_stream.write(static_cast<const char*>(data), dataSize);
m_current_offset += dataSize;
}
void Pad(const size_t paddingSize)
{
auto paddingSizeLeft = paddingSize;
while (paddingSizeLeft > 0)
{
const auto writeSize = std::min(paddingSizeLeft, PAD_DATA.size());
Write(PAD_DATA.data(), writeSize);
paddingSizeLeft -= writeSize;
}
}
void AlignToChunk()
{
if (m_current_offset % 16 != 0)
Pad(16 - (m_current_offset % 16));
}
void WriteHeader()
{
GoTo(0);
// The checksum here is supposed to be a MD5 of the entire data portion of the file. However T6 does not validate this.
// As long as the values here match the values in the SndBank asset, everything loads fine
SoundAssetBankChecksum checksum{};
memset(&checksum, 0xCC, sizeof(SoundAssetBankChecksum));
SoundAssetBankHeader header{
MAGIC,
VERSION,
sizeof(SoundAssetBankEntry),
sizeof(SoundAssetBankChecksum),
0x40,
m_entries.size(),
0,
0,
m_total_size,
m_entry_section_offset,
m_checksum_section_offset,
checksum,
{},
};
strncpy(header.dependencies, m_file_name.data(), header.dependencySize);
Write(&header, sizeof(header));
}
bool WriteEntries()
{
GoTo(DATA_OFFSET);
for (auto& sound : m_sounds)
{
const auto& soundFilePath = sound.m_file_path;
const auto soundId = sound.m_sound_id;
size_t soundSize;
std::unique_ptr<char[]> soundData;
// try to find a wav file for the sound path
const auto wavFile = m_asset_search_path->Open(soundFilePath + ".wav");
if (wavFile.IsOpen())
{
WavHeader header{};
wavFile.m_stream->read(reinterpret_cast<char*>(&header), sizeof(WavHeader));
soundSize = static_cast<size_t>(wavFile.m_length - sizeof(WavHeader));
const auto frameCount = soundSize / (header.formatChunk.nChannels * (header.formatChunk.wBitsPerSample / 8));
const auto frameRateIndex = INDEX_FOR_FRAMERATE[header.formatChunk.nSamplesPerSec];
SoundAssetBankEntry entry{
soundId,
soundSize,
static_cast<size_t>(m_current_offset),
frameCount,
frameRateIndex,
static_cast<unsigned char>(header.formatChunk.nChannels),
sound.m_looping,
0,
};
m_entries.push_back(entry);
soundData = std::make_unique<char[]>(soundSize);
wavFile.m_stream->read(soundData.get(), soundSize);
}
else
{
// if there is no wav file, try flac file
const auto flacFile = m_asset_search_path->Open(soundFilePath + ".flac");
if (flacFile.IsOpen())
{
soundSize = static_cast<size_t>(flacFile.m_length);
soundData = std::make_unique<char[]>(soundSize);
flacFile.m_stream->read(soundData.get(), soundSize);
flac::FlacMetaData metaData;
if (flac::GetFlacMetaData(soundData.get(), soundSize, metaData))
{
const auto frameRateIndex = INDEX_FOR_FRAMERATE[metaData.m_sample_rate];
SoundAssetBankEntry entry{
soundId,
soundSize,
static_cast<size_t>(m_current_offset),
static_cast<unsigned>(metaData.m_total_samples),
frameRateIndex,
metaData.m_number_of_channels,
sound.m_looping,
8,
};
m_entries.push_back(entry);
}
else
{
std::cerr << "Unable to decode .flac file for sound " << soundFilePath << std::endl;
return false;
}
}
else
{
std::cerr << "Unable to find a compatible file for sound " << soundFilePath << std::endl;
return false;
}
}
const auto lastEntry = m_entries.rbegin();
if (!sound.m_streamed && lastEntry->frameRateIndex != 6)
{
std::cout << "WARNING: Loaded sound \"" << soundFilePath
<< "\" should have a framerate of 48000 but doesn't. This sound may not work on all games!" << std::endl;
}
// calculate checksum
SoundAssetBankChecksum checksum{};
const auto md5Crypt = Crypto::CreateMD5();
md5Crypt->Process(soundData.get(), soundSize);
md5Crypt->Finish(checksum.checksumBytes);
m_checksums.push_back(checksum);
// write data
Write(soundData.get(), soundSize);
}
return true;
}
void WriteEntryList()
{
AlignToChunk();
m_entry_section_offset = m_current_offset;
for (auto& entry : m_entries)
{
Write(&entry, sizeof(SoundAssetBankEntry));
}
}
void WriteChecksumList()
{
m_checksum_section_offset = m_current_offset;
for (auto& checksum : m_checksums)
{
Write(&checksum, sizeof(SoundAssetBankChecksum));
}
}
void WriteBranding()
{
AlignToChunk();
Write(BRANDING, sizeof(BRANDING));
AlignToChunk();
}
bool Write(size_t& dataSize) override
{
if (!WriteEntries())
{
std::cerr << "An error occurred writing the sound bank entries. Please check output." << std::endl;
return false;
}
WriteEntryList();
WriteChecksumList();
WriteBranding();
m_total_size = m_current_offset;
WriteHeader();
if (m_current_offset > UINT32_MAX)
{
std::cerr << "Sound bank files must be under 4GB. Please reduce the number of sounds being written!" << std::endl;
return false;
}
// output the total size for the sound asset data
dataSize = static_cast<size_t>(m_entry_section_offset - DATA_OFFSET);
return true;
}
private:
class SoundBankEntryInfo
{
public:
SoundBankEntryInfo()
: m_sound_id(0u),
m_looping(false),
m_streamed(false)
{
}
SoundBankEntryInfo(std::string filePath, const unsigned int soundId, const bool looping, const bool streamed)
: m_file_path(std::move(filePath)),
m_sound_id(soundId),
m_looping(looping),
m_streamed(streamed)
{
}
std::string m_file_path;
unsigned int m_sound_id;
bool m_looping;
bool m_streamed;
};
std::string m_file_name;
std::ostream& m_stream;
ISearchPath* m_asset_search_path;
std::vector<SoundBankEntryInfo> m_sounds;
int64_t m_current_offset;
std::vector<SoundAssetBankEntry> m_entries;
std::vector<SoundAssetBankChecksum> m_checksums;
int64_t m_total_size;
int64_t m_entry_section_offset;
int64_t m_checksum_section_offset;
};
std::filesystem::path SoundBankWriter::OutputPath;
std::unique_ptr<SoundBankWriter> SoundBankWriter::Create(const std::string& fileName, std::ostream& stream, ISearchPath* assetSearchPath)
{
return std::make_unique<SoundBankWriterImpl>(fileName, stream, assetSearchPath);
}

View File

@ -0,0 +1,25 @@
#pragma once
#include "SearchPath/ISearchPath.h"
#include <filesystem>
#include <memory>
#include <ostream>
class SoundBankWriter
{
public:
SoundBankWriter() = default;
virtual ~SoundBankWriter() = default;
SoundBankWriter(const SoundBankWriter& other) = default;
SoundBankWriter(SoundBankWriter&& other) noexcept = default;
SoundBankWriter& operator=(const SoundBankWriter& other) = default;
SoundBankWriter& operator=(SoundBankWriter&& other) noexcept = default;
virtual void AddSound(const std::string& soundFilePath, unsigned int soundId, bool looping = false, bool streamed = false) = 0;
virtual bool Write(size_t& dataSize) = 0;
static std::unique_ptr<SoundBankWriter> Create(const std::string& fileName, std::ostream& stream, ISearchPath* assetSearchPath);
static std::filesystem::path OutputPath;
};

View File

@ -52,7 +52,8 @@ function ObjWriting:project()
self:include(includes) self:include(includes)
Utils:include(includes) Utils:include(includes)
json:include(includes)
minilzo:include(includes) minilzo:include(includes)
minizip:include(includes) minizip:include(includes)
json:include(includes)
end end

View File

@ -2,6 +2,7 @@
#include "Csv/CsvStream.h" #include "Csv/CsvStream.h"
#include "Game/T6/CommonT6.h" #include "Game/T6/CommonT6.h"
#include "Game/T6/SoundConstantsT6.h"
#include "ObjContainer/SoundBank/SoundBank.h" #include "ObjContainer/SoundBank/SoundBank.h"
#include "Sound/WavWriter.h" #include "Sound/WavWriter.h"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
@ -51,6 +52,7 @@ namespace
"start_delay", "start_delay",
"reverb_send", "reverb_send",
"duck", "duck",
"duck_group",
"pan", "pan",
"center_send", "center_send",
"envelop_min", "envelop_min",
@ -118,147 +120,15 @@ namespace
192000, 192000,
}; };
const std::string GROUPS_ENUM[]{
"grp_reference",
"grp_master",
"grp_wpn_lfe",
"grp_lfe",
"grp_hdrfx",
"grp_music",
"grp_voice",
"grp_set_piece",
"grp_igc",
"grp_mp_game",
"grp_explosion",
"grp_player_impacts",
"grp_scripted_moment",
"grp_menu",
"grp_whizby",
"grp_weapon",
"grp_vehicle",
"grp_impacts",
"grp_foley",
"grp_destructible",
"grp_physics",
"grp_ambience",
"grp_alerts",
"grp_air",
"grp_bink",
"grp_announcer",
"",
};
const std::string CURVES_ENUM[]{
"default",
"defaultmin",
"allon",
"alloff",
"rcurve0",
"rcurve1",
"rcurve2",
"rcurve3",
"rcurve4",
"rcurve5",
"steep",
"sindelay",
"cosdelay",
"sin",
"cos",
"rev60",
"rev65",
"",
};
std::unordered_map<unsigned int, std::string> CreateCurvesMap() std::unordered_map<unsigned int, std::string> CreateCurvesMap()
{ {
std::unordered_map<unsigned int, std::string> result; std::unordered_map<unsigned int, std::string> result;
for (auto i = 0u; i < std::extent_v<decltype(CURVES_ENUM)>; i++) for (auto i = 0u; i < std::extent_v<decltype(SOUND_CURVES)>; i++)
result.emplace(T6::Common::SND_HashName(CURVES_ENUM[i].data()), CURVES_ENUM[i]); result.emplace(T6::Common::SND_HashName(SOUND_CURVES[i].data()), SOUND_CURVES[i]);
return result; return result;
} }
const std::unordered_map<unsigned int, std::string> CURVES_MAP = CreateCurvesMap(); const std::unordered_map<unsigned int, std::string> CURVES_MAP = CreateCurvesMap();
const std::string DUCK_GROUPS_ENUM[]{
"snp_alerts_gameplay",
"snp_ambience",
"snp_claw",
"snp_destructible",
"snp_dying",
"snp_dying_ice",
"snp_evt_2d",
"snp_explosion",
"snp_foley",
"snp_grenade",
"snp_hdrfx",
"snp_igc",
"snp_impacts",
"snp_menu",
"snp_movie",
"snp_music",
"snp_never_duck",
"snp_player_dead",
"snp_player_impacts",
"snp_scripted_moment",
"snp_set_piece",
"snp_special",
"snp_vehicle",
"snp_vehicle_interior",
"snp_voice",
"snp_weapon_decay_1p",
"snp_whizby",
"snp_wpn_1p",
"snp_wpn_3p",
"snp_wpn_turret",
"snp_x2",
"snp_x3",
};
const std::string LIMIT_TYPES_ENUM[]{
"none",
"oldest",
"reject",
"priority",
};
const std::string MOVE_TYPES_ENUM[]{
"none",
"left_player",
"center_player",
"right_player",
"random",
"left_shot",
"center_shot",
"right_shot",
};
const std::string LOAD_TYPES_ENUM[]{
"unknown",
"loaded",
"streamed",
"primed",
};
const std::string BUS_IDS_ENUM[]{
"bus_reverb",
"bus_fx",
"bus_voice",
"bus_pfutz",
"bus_hdrfx",
"bus_ui",
"bus_reference",
"bus_music",
"bus_movie",
"bus_reference",
"",
};
const std::string RANDOMIZE_TYPES_ENUM[]{
"volume",
"pitch",
"variant",
"",
};
} // namespace } // namespace
class AssetDumperSndBank::Internal class AssetDumperSndBank::Internal
@ -344,7 +214,7 @@ class AssetDumperSndBank::Internal
stream.WriteColumn(alias->name); stream.WriteColumn(alias->name);
// file // file
stream.WriteColumn(alias->assetFileName); stream.WriteColumn(alias->assetFileName ? alias->assetFileName : "");
// template // template
stream.WriteColumn(""); stream.WriteColumn("");
@ -356,7 +226,7 @@ class AssetDumperSndBank::Internal
stream.WriteColumn((alias->secondaryname && *alias->secondaryname) ? alias->secondaryname : ""); stream.WriteColumn((alias->secondaryname && *alias->secondaryname) ? alias->secondaryname : "");
// group // group
stream.WriteColumn(GROUPS_ENUM[alias->flags.volumeGroup]); stream.WriteColumn(SOUND_GROUPS[alias->flags.volumeGroup]);
// vol_min // vol_min
stream.WriteColumn(std::to_string(alias->volMin)); stream.WriteColumn(std::to_string(alias->volMin));
@ -377,28 +247,28 @@ class AssetDumperSndBank::Internal
stream.WriteColumn(std::to_string(alias->distReverbMax)); stream.WriteColumn(std::to_string(alias->distReverbMax));
// volume_falloff_curve // volume_falloff_curve
stream.WriteColumn(CURVES_ENUM[alias->flags.volumeFalloffCurve]); stream.WriteColumn(SOUND_CURVES[alias->flags.volumeFalloffCurve]);
// reverb_falloff_curve // reverb_falloff_curve
stream.WriteColumn(CURVES_ENUM[alias->flags.reverbFalloffCurve]); stream.WriteColumn(SOUND_CURVES[alias->flags.reverbFalloffCurve]);
// volume_min_falloff_curve // volume_min_falloff_curve
stream.WriteColumn(CURVES_ENUM[alias->flags.volumeMinFalloffCurve]); stream.WriteColumn(SOUND_CURVES[alias->flags.volumeMinFalloffCurve]);
// reverb_min_falloff_curve" // reverb_min_falloff_curve"
stream.WriteColumn(CURVES_ENUM[alias->flags.reverbMinFalloffCurve]); stream.WriteColumn(SOUND_CURVES[alias->flags.reverbMinFalloffCurve]);
// limit_count // limit_count
stream.WriteColumn(std::to_string(alias->limitCount)); stream.WriteColumn(std::to_string(alias->limitCount));
// limit_type // limit_type
stream.WriteColumn(LIMIT_TYPES_ENUM[alias->flags.limitType]); stream.WriteColumn(SOUND_LIMIT_TYPES[alias->flags.limitType]);
// entity_limit_count // entity_limit_count
stream.WriteColumn(std::to_string(alias->entityLimitCount)); stream.WriteColumn(std::to_string(alias->entityLimitCount));
// entity_limit_type // entity_limit_type
stream.WriteColumn(LIMIT_TYPES_ENUM[alias->flags.entityLimitType]); stream.WriteColumn(SOUND_LIMIT_TYPES[alias->flags.entityLimitType]);
// pitch_min // pitch_min
stream.WriteColumn(std::to_string(alias->pitchMin)); stream.WriteColumn(std::to_string(alias->pitchMin));
@ -425,13 +295,13 @@ class AssetDumperSndBank::Internal
stream.WriteColumn(""); stream.WriteColumn("");
// type // type
stream.WriteColumn(LOAD_TYPES_ENUM[alias->flags.loadType]); stream.WriteColumn(SOUND_LOAD_TYPES[alias->flags.loadType]);
// loop // loop
stream.WriteColumn(alias->flags.looping == T6::SA_NON_LOOPING ? "nonlooping" : "looping"); stream.WriteColumn(alias->flags.looping == T6::SA_NON_LOOPING ? "nonlooping" : "looping");
// randomize_type // randomize_type
stream.WriteColumn(RANDOMIZE_TYPES_ENUM[alias->flags.randomizeType]); stream.WriteColumn(SOUND_RANDOMIZE_TYPES[std::min(alias->flags.randomizeType, 3u)]);
// probability", // probability",
stream.WriteColumn(std::to_string(alias->probability)); stream.WriteColumn(std::to_string(alias->probability));
@ -445,6 +315,9 @@ class AssetDumperSndBank::Internal
// duck", // duck",
stream.WriteColumn(FindNameForDuck(alias->duck, bank)); stream.WriteColumn(FindNameForDuck(alias->duck, bank));
// duck_group",
stream.WriteColumn(SOUND_DUCK_GROUPS[alias->duckGroup]);
// pan", // pan",
stream.WriteColumn(alias->flags.panType == SA_PAN_2D ? "2d" : "3d"); stream.WriteColumn(alias->flags.panType == SA_PAN_2D ? "2d" : "3d");
@ -473,7 +346,7 @@ class AssetDumperSndBank::Internal
stream.WriteColumn(alias->flags.distanceLpf ? "yes" : "no"); stream.WriteColumn(alias->flags.distanceLpf ? "yes" : "no");
// move_type", // move_type",
stream.WriteColumn(MOVE_TYPES_ENUM[alias->flags.fluxType]); stream.WriteColumn(SOUND_MOVE_TYPES[alias->flags.fluxType]);
// move_time", // move_time",
stream.WriteColumn(std::to_string(alias->fluxTime)); stream.WriteColumn(std::to_string(alias->fluxTime));
@ -524,7 +397,7 @@ class AssetDumperSndBank::Internal
stream.WriteColumn(alias->flags.stopOnDeath ? "yes" : "no"); stream.WriteColumn(alias->flags.stopOnDeath ? "yes" : "no");
// bus", // bus",
stream.WriteColumn(BUS_IDS_ENUM[alias->flags.busType]); stream.WriteColumn(SOUND_BUS_IDS[alias->flags.busType]);
// snapshot", // snapshot",
stream.WriteColumn(""); stream.WriteColumn("");
@ -647,13 +520,14 @@ class AssetDumperSndBank::Internal
for (auto j = 0; j < aliasList.count; j++) for (auto j = 0; j < aliasList.count; j++)
{ {
const auto& alias = aliasList.head[j]; const auto& alias = aliasList.head[j];
WriteAliasToFile(csvStream, &alias, sndBank);
csvStream.NextRow();
if (alias.assetId && alias.assetFileName && dumpedAssets.find(alias.assetId) == dumpedAssets.end()) if (alias.assetId && alias.assetFileName && dumpedAssets.find(alias.assetId) == dumpedAssets.end())
{ {
DumpSndAlias(alias); DumpSndAlias(alias);
dumpedAssets.emplace(alias.assetId); dumpedAssets.emplace(alias.assetId);
WriteAliasToFile(csvStream, &alias, sndBank);
csvStream.NextRow();
} }
} }
} }
@ -757,8 +631,8 @@ class AssetDumperSndBank::Internal
for (auto i = 0u; i < 32u; i++) for (auto i = 0u; i < 32u; i++)
{ {
values.push_back({ values.push_back({
{"duckGroup", DUCK_GROUPS_ENUM[i]}, {"duckGroup", SOUND_DUCK_GROUPS[i]},
{"attenuation", duck.attenuation[i]}, {"attenuation", duck.attenuation[i] },
{"filter", duck.filter[i] } {"filter", duck.filter[i] }
}); });
} }

View File

@ -1,7 +1,5 @@
#include "WavWriter.h" #include "WavWriter.h"
#include "Sound/WavTypes.h"
WavWriter::WavWriter(std::ostream& stream) WavWriter::WavWriter(std::ostream& stream)
: m_stream(stream) : m_stream(stream)
{ {

View File

@ -1,12 +1,7 @@
#pragma once #pragma once
#include <ostream> #include "Sound/WavTypes.h"
struct WavMetaData #include <ostream>
{
unsigned channelCount;
unsigned samplesPerSec;
unsigned bitsPerSample;
};
class WavWriter class WavWriter
{ {

View File

@ -1,5 +1,6 @@
#include "StringUtils.h" #include "StringUtils.h"
#include <iostream>
#include <sstream> #include <sstream>
namespace utils namespace utils
@ -100,4 +101,18 @@ namespace utils
for (auto& c : str) for (auto& c : str)
c = static_cast<char>(toupper(static_cast<unsigned char>(c))); c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
} }
std::vector<std::string> StringSplit(const std::string& str, const char delim)
{
std::vector<std::string> strings;
std::istringstream stream(str);
std::string s;
while (std::getline(stream, s, delim))
{
strings.emplace_back(std::move(s));
}
return strings;
}
} // namespace utils } // namespace utils

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <string> #include <string>
#include <vector>
namespace utils namespace utils
{ {
@ -14,4 +15,6 @@ namespace utils
void MakeStringLowerCase(std::string& str); void MakeStringLowerCase(std::string& str);
void MakeStringUpperCase(std::string& str); void MakeStringUpperCase(std::string& str);
std::vector<std::string> StringSplit(const std::string& str, const char delim);
} // namespace utils } // namespace utils

View File

@ -0,0 +1,39 @@
#include "Sound/FlacDecoder.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
namespace flac
{
TEST_CASE("FlacDecoder: Ensure properly decodes flac stream info", "[sound][flac]")
{
// clang-format off
constexpr uint8_t testData[]
{
// Magic
'f', 'L', 'a', 'C',
// Block header
0x00, 0x00, 0x00, 0x22,
// StreamInfo block
0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0xB8, 0x02, 0xF0, 0x00,
0x02, 0xF9, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
// clang-format on
FlacMetaData metaData;
const auto result = GetFlacMetaData(testData, sizeof(testData), metaData);
REQUIRE(result == true);
REQUIRE(metaData.m_minimum_block_size == 1024);
REQUIRE(metaData.m_maximum_block_size == 1024);
REQUIRE(metaData.m_minimum_frame_size == 0);
REQUIRE(metaData.m_maximum_frame_size == 0);
REQUIRE(metaData.m_sample_rate == 48000);
REQUIRE(metaData.m_number_of_channels == 2);
REQUIRE(metaData.m_bits_per_sample == 16);
REQUIRE(metaData.m_total_samples == 194870);
}
} // namespace flac