diff --git a/src/Crypto/Crypto.cpp b/src/Crypto/Crypto.cpp index 4ee69ce4..3dc07fd7 100644 --- a/src/Crypto/Crypto.cpp +++ b/src/Crypto/Crypto.cpp @@ -1,10 +1,16 @@ #include "Crypto.h" +#include "Impl/AlgorithmMD5.h" #include "Impl/AlgorithmRSA.h" #include "Impl/AlgorithmSHA1.h" #include "Impl/AlgorithmSHA256.h" #include "Impl/AlgorithmSalsa20.h" +std::unique_ptr Crypto::CreateMD5() +{ + return std::make_unique(); +} + std::unique_ptr Crypto::CreateSHA1() { return std::make_unique(); diff --git a/src/Crypto/Crypto.h b/src/Crypto/Crypto.h index 40f99752..e7e5edb8 100644 --- a/src/Crypto/Crypto.h +++ b/src/Crypto/Crypto.h @@ -16,6 +16,8 @@ public: RSA_PADDING_PSS, }; + static std::unique_ptr CreateMD5(); + static std::unique_ptr CreateSHA1(); static std::unique_ptr CreateSHA256(); diff --git a/src/Crypto/Impl/AlgorithmMD5.cpp b/src/Crypto/Impl/AlgorithmMD5.cpp new file mode 100644 index 00000000..3e14e74b --- /dev/null +++ b/src/Crypto/Impl/AlgorithmMD5.cpp @@ -0,0 +1,64 @@ +#include "AlgorithmMD5.h" + +#include "CryptoLibrary.h" + +#include + +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(input), inputSize); + } + + void Finish(void* hashBuffer) + { + md5_done(&m_state, static_cast(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); +} diff --git a/src/Crypto/Impl/AlgorithmMD5.h b/src/Crypto/Impl/AlgorithmMD5.h new file mode 100644 index 00000000..4687572a --- /dev/null +++ b/src/Crypto/Impl/AlgorithmMD5.h @@ -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; +}; diff --git a/src/Linker/Linker.cpp b/src/Linker/Linker.cpp index 6c05aa65..e9587c11 100644 --- a/src/Linker/Linker.cpp +++ b/src/Linker/Linker.cpp @@ -9,6 +9,7 @@ #include "LinkerSearchPaths.h" #include "ObjContainer/IPak/IPakWriter.h" #include "ObjContainer/IWD/IWD.h" +#include "ObjContainer/SoundBank/SoundBankWriter.h" #include "ObjLoading.h" #include "ObjWriting.h" #include "SearchPath/SearchPaths.h" @@ -419,6 +420,8 @@ class LinkerImpl final : public Linker SearchPaths& gdtSearchPaths, SearchPaths& sourceSearchPaths) const { + SoundBankWriter::OutputPath = fs::path(m_args.GetOutputFolderPathForProject(projectName)); + const auto zone = CreateZoneForDefinition(targetName, zoneDefinition, &assetSearchPaths, &gdtSearchPaths, &sourceSearchPaths); auto result = zone != nullptr; if (zone) diff --git a/src/ObjCommon/Csv/ParsedCsv.cpp b/src/ObjCommon/Csv/ParsedCsv.cpp new file mode 100644 index 00000000..c54ce5a5 --- /dev/null +++ b/src/ObjCommon/Csv/ParsedCsv.cpp @@ -0,0 +1,80 @@ +#include "Csv/ParsedCsv.h" + +ParsedCsvRow::ParsedCsvRow(std::unordered_map& headers, std::vector 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> csvLines; + std::vector currentLine; + + while (inputStream.NextRow(currentLine)) + { + csvLines.emplace_back(std::move(currentLine)); + currentLine = std::vector(); + } + + 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); +} diff --git a/src/ObjCommon/Csv/ParsedCsv.h b/src/ObjCommon/Csv/ParsedCsv.h new file mode 100644 index 00000000..6913c80c --- /dev/null +++ b/src/ObjCommon/Csv/ParsedCsv.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Csv/CsvStream.h" +#include "Utils/ClassUtils.h" + +#include +#include + +class ParsedCsvRow +{ + std::unordered_map& headers; + std::vector values; + +public: + explicit ParsedCsvRow(std::unordered_map& headers, std::vector 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 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(out); + } + + return {}; + } +}; + +class ParsedCsv +{ + std::unordered_map headers; + std::vector rows; + +public: + explicit ParsedCsv(const CsvInputStream& inputStream, bool hasHeaders = true); + + _NODISCARD size_t Size() const; + + ParsedCsvRow operator[](size_t index) const; +}; diff --git a/src/ObjCommon/Game/T6/SoundConstantsT6.h b/src/ObjCommon/Game/T6/SoundConstantsT6.h new file mode 100644 index 00000000..feb7cff6 --- /dev/null +++ b/src/ObjCommon/Game/T6/SoundConstantsT6.h @@ -0,0 +1,137 @@ +#pragma once +#include + +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 diff --git a/src/ObjLoading/ObjContainer/SoundBank/SoundBankTypes.h b/src/ObjCommon/ObjContainer/SoundBank/SoundBankTypes.h similarity index 100% rename from src/ObjLoading/ObjContainer/SoundBank/SoundBankTypes.h rename to src/ObjCommon/ObjContainer/SoundBank/SoundBankTypes.h diff --git a/src/ObjCommon/Sound/FlacDecoder.cpp b/src/ObjCommon/Sound/FlacDecoder.cpp new file mode 100644 index 00000000..b709029f --- /dev/null +++ b/src/ObjCommon/Sound/FlacDecoder.cpp @@ -0,0 +1,246 @@ +#include "FlacDecoder.h" + +#include "Utils/Alignment.h" +#include "Utils/ClassUtils.h" +#include "Utils/Endianness.h" +#include "Utils/FileUtils.h" + +#include +#include +#include + +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 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(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(m_last_byte << (8u - bitsFromFirstByte)); + + m_stream.read(reinterpret_cast(&m_last_byte), sizeof(m_last_byte)); + if (m_stream.gcount() != sizeof(m_last_byte)) + throw FlacReadingException("Unexpected eof"); + + const auto bitsFromSecondByte = static_cast(curBits - m_remaining_bits_last_byte); + m_remaining_bits_last_byte = 8u - bitsFromSecondByte; + const auto maskForSecondByte = static_cast(0xFF << (8u - bitsFromSecondByte)); + data.buffer[offset] |= (m_last_byte & maskForSecondByte) >> bitsFromFirstByte; + } + else if (m_remaining_bits_last_byte == curBits) + { + data.buffer[offset] = static_cast(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(maskForCurBits << (m_remaining_bits_last_byte - curBits)); + const auto selectedData = static_cast(m_last_byte & maskForCurBitsInRemainingBits); + data.buffer[offset] = static_cast(selectedData << (8u - m_remaining_bits_last_byte)); + m_remaining_bits_last_byte -= curBits; + } + } + else if (curBits >= 8u) + { + m_stream.read(reinterpret_cast(&data.buffer[offset]), sizeof(uint8_t)); + if (m_stream.gcount() != sizeof(uint8_t)) + throw FlacReadingException("Unexpected eof"); + } + else + { + m_stream.read(reinterpret_cast(&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(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(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(16); + metaData.m_maximum_block_size = reader.ReadBits(16); + metaData.m_minimum_frame_size = reader.ReadBits(24); + metaData.m_maximum_frame_size = reader.ReadBits(24); + metaData.m_sample_rate = reader.ReadBits(20); + metaData.m_number_of_channels = static_cast(reader.ReadBits(3) + 1); + metaData.m_bits_per_sample = static_cast(reader.ReadBits(5) + 1); + metaData.m_total_samples = reader.ReadBits(36); + reader.ReadBuffer(metaData.m_md5_signature, 128); + } + + bool GetFlacMetaData(std::istream& stream, FlacMetaData& metaData) + { + try + { + uint32_t readMagic; + stream.read(reinterpret_cast(&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(1), + static_cast(reader.ReadBits(7)), + reader.ReadBits(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(data), dataSize)); + return GetFlacMetaData(ss, metaData); + } +} // namespace flac diff --git a/src/ObjCommon/Sound/FlacDecoder.h b/src/ObjCommon/Sound/FlacDecoder.h new file mode 100644 index 00000000..cec71975 --- /dev/null +++ b/src/ObjCommon/Sound/FlacDecoder.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +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 diff --git a/src/ObjCommon/Sound/WavTypes.h b/src/ObjCommon/Sound/WavTypes.h index 18dc14b9..1a997816 100644 --- a/src/ObjCommon/Sound/WavTypes.h +++ b/src/ObjCommon/Sound/WavTypes.h @@ -28,3 +28,20 @@ struct WavFormatChunkPcm uint16_t nBlockAlign; 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; +}; diff --git a/src/ObjLoading.lua b/src/ObjLoading.lua index c9109ae5..30605cdb 100644 --- a/src/ObjLoading.lua +++ b/src/ObjLoading.lua @@ -55,4 +55,5 @@ function ObjLoading:project() minilzo:include(includes) minizip:include(includes) zlib:include(includes) + json:include(includes) end diff --git a/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderSoundBank.cpp b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderSoundBank.cpp new file mode 100644 index 00000000..c0b12fb4 --- /dev/null +++ b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderSoundBank.cpp @@ -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 +#include +#include +#include + +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 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(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(); + 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("vol_min"); + alias->volMax = row.GetValueInt("vol_max"); + alias->distMin = row.GetValueInt("dist_min"); + alias->distMax = row.GetValueInt("dist_max"); + alias->distReverbMax = row.GetValueInt("dist_reverb_max"); + alias->limitCount = row.GetValueInt("limit_count"); + alias->entityLimitCount = row.GetValueInt("entity_limit_count"); + alias->pitchMin = row.GetValueInt("pitch_min"); + alias->pitchMax = row.GetValueInt("pitch_max"); + alias->minPriority = row.GetValueInt("min_priority"); + alias->maxPriority = row.GetValueInt("max_priority"); + alias->minPriorityThreshold = row.GetValueInt("min_priority_threshold"); + alias->maxPriorityThreshold = row.GetValueInt("max_priority_threshold"); + alias->probability = row.GetValueInt("probability"); + alias->startDelay = row.GetValueInt("start_delay"); + alias->reverbSend = row.GetValueInt("reverb_send"); + alias->centerSend = row.GetValueInt("center_send"); + alias->envelopMin = row.GetValueInt("envelop_min"); + alias->envelopMax = row.GetValueInt("envelop_max"); + alias->envelopPercentage = row.GetValueInt("envelop_percentage"); + alias->occlusionLevel = row.GetValueInt("occlusion_level"); + alias->fluxTime = row.GetValueInt("move_time"); + alias->futzPatch = row.GetValueInt("futz"); + alias->contextType = row.GetValueInt("context_type"); + alias->contextValue = row.GetValueInt("context_value"); + alias->fadeIn = row.GetValueInt("fade_in"); + alias->fadeOut = row.GetValueInt("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(GetValueIndex(row.GetValue("duck_group"), SOUND_DUCK_GROUPS, std::extent_v)); + alias->flags.volumeGroup = GetValueIndex(row.GetValue("group"), SOUND_GROUPS, std::extent_v); + alias->flags.fluxType = GetValueIndex(row.GetValue("move_type"), SOUND_MOVE_TYPES, std::extent_v); + alias->flags.loadType = GetValueIndex(row.GetValue("type"), SOUND_LOAD_TYPES, std::extent_v); + alias->flags.busType = GetValueIndex(row.GetValue("bus"), SOUND_BUS_IDS, std::extent_v); + alias->flags.limitType = GetValueIndex(row.GetValue("limit_type"), SOUND_LIMIT_TYPES, std::extent_v); + alias->flags.volumeFalloffCurve = GetValueIndex(row.GetValue("volume_falloff_curve"), SOUND_CURVES, std::extent_v); + alias->flags.reverbFalloffCurve = GetValueIndex(row.GetValue("reverb_falloff_curve"), SOUND_CURVES, std::extent_v); + alias->flags.entityLimitType = GetValueIndex(row.GetValue("entity_limit_type"), SOUND_LIMIT_TYPES, std::extent_v); + alias->flags.volumeMinFalloffCurve = GetValueIndex(row.GetValue("volume_min_falloff_curve"), SOUND_CURVES, std::extent_v); + alias->flags.reverbMinFalloffCurve = GetValueIndex(row.GetValue("reverb_min_falloff_curve"), SOUND_CURVES, std::extent_v); + alias->flags.randomizeType = GetValueIndex(row.GetValue("randomize_type"), SOUND_RANDOMIZE_TYPES, std::extent_v); + + return true; +} + +bool LoadSoundAliasIndexList(MemoryManager* memory, SndBank* sndBank) +{ + // contains a list of all the alias ids in the sound bank + sndBank->aliasIndex = static_cast(memory->Alloc(sizeof(SndIndexEntry) * sndBank->aliasCount)); + memset(sndBank->aliasIndex, 0xFF, sizeof(SndIndexEntry) * sndBank->aliasCount); + + const auto setAliasIndexList = std::make_unique(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::max()) + { + sndBank->aliasIndex[idx].value = i; + sndBank->aliasIndex[idx].next = std::numeric_limits::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::max()) + { + idx = sndBank->aliasIndex[idx].next; + } + + auto offset = 1u; + auto freeIdx = std::numeric_limits::max(); + while (true) + { + freeIdx = (idx + offset) % sndBank->aliasCount; + if (sndBank->aliasIndex[freeIdx].value == std::numeric_limits::max()) + break; + + freeIdx = (idx + sndBank->aliasCount - offset) % sndBank->aliasCount; + if (sndBank->aliasIndex[freeIdx].value == std::numeric_limits::max()) + break; + + offset++; + freeIdx = std::numeric_limits::max(); + + if (offset >= sndBank->aliasCount) + break; + } + + if (freeIdx == std::numeric_limits::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::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(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(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(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(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(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(); + duck->fadeOut = duckJson["fadeOut"].get(); + duck->startDelay = duckJson["startDelay"].get(); + duck->distance = duckJson["distance"].get(); + duck->length = duckJson["length"].get(); + duck->updateWhilePaused = duckJson["updateWhilePaused"].get(); + + duck->fadeInCurve = duckJson["fadeInCurveId"].get(); + duck->fadeOutCurve = duckJson["fadeOutCurveId"].get(); + + if (duckJson.contains("fadeInCurve")) + duck->fadeInCurve = Common::SND_HashName(duckJson["fadeInCurve"].get().data()); + + if (duckJson.contains("fadeOutCurve")) + duck->fadeOutCurve = Common::SND_HashName(duckJson["fadeOutCurve"].get().data()); + + duck->attenuation = static_cast(memory->Alloc(sizeof(SndFloatAlign16) * 32)); + duck->filter = static_cast(memory->Alloc(sizeof(SndFloatAlign16) * 32)); + + for (auto& valueJson : duckJson["values"]) + { + auto index = GetValueIndex(valueJson["duckGroup"].get(), SOUND_DUCK_GROUPS, std::extent_v); + + duck->attenuation[index] = valueJson["attenuation"].get(); + duck->filter[index] = valueJson["filter"].get(); + } + } + } + + 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(); + 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 sablStream, sabsStream; + std::unique_ptr 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(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(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; +} diff --git a/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderSoundBank.h b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderSoundBank.h new file mode 100644 index 00000000..f7f8154d --- /dev/null +++ b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderSoundBank.h @@ -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 + { + 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 diff --git a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp index 431a3ca4..dcff00e5 100644 --- a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp +++ b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp @@ -9,6 +9,7 @@ #include "AssetLoaders/AssetLoaderRawFile.h" #include "AssetLoaders/AssetLoaderScriptParseTree.h" #include "AssetLoaders/AssetLoaderSlug.h" +#include "AssetLoaders/AssetLoaderSoundBank.h" #include "AssetLoaders/AssetLoaderStringTable.h" #include "AssetLoaders/AssetLoaderTracer.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_TECHNIQUE_SET, MaterialTechniqueSet)) 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_CLIPMAP, clipMap_t)) REGISTER_ASSET_LOADER(BASIC_LOADER(ASSET_TYPE_CLIPMAP_PVS, clipMap_t)) diff --git a/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.cpp b/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.cpp new file mode 100644 index 00000000..153a75ba --- /dev/null +++ b/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.cpp @@ -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 +#include +#include +#include + +std::unordered_map 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(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 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(&header), sizeof(WavHeader)); + + soundSize = static_cast(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(m_current_offset), + frameCount, + frameRateIndex, + static_cast(header.formatChunk.nChannels), + sound.m_looping, + 0, + }; + + m_entries.push_back(entry); + + soundData = std::make_unique(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(flacFile.m_length); + + soundData = std::make_unique(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(m_current_offset), + static_cast(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(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 m_sounds; + + int64_t m_current_offset; + std::vector m_entries; + std::vector 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::Create(const std::string& fileName, std::ostream& stream, ISearchPath* assetSearchPath) +{ + return std::make_unique(fileName, stream, assetSearchPath); +} diff --git a/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.h b/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.h new file mode 100644 index 00000000..ccd338b3 --- /dev/null +++ b/src/ObjLoading/ObjContainer/SoundBank/SoundBankWriter.h @@ -0,0 +1,25 @@ +#pragma once +#include "SearchPath/ISearchPath.h" + +#include +#include +#include + +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 Create(const std::string& fileName, std::ostream& stream, ISearchPath* assetSearchPath); + + static std::filesystem::path OutputPath; +}; diff --git a/src/ObjWriting.lua b/src/ObjWriting.lua index cf0ef668..8505cd8b 100644 --- a/src/ObjWriting.lua +++ b/src/ObjWriting.lua @@ -52,7 +52,8 @@ function ObjWriting:project() self:include(includes) Utils:include(includes) - json:include(includes) minilzo:include(includes) minizip:include(includes) + json:include(includes) + end diff --git a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperSndBank.cpp b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperSndBank.cpp index b36a4f3c..c2ae698b 100644 --- a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperSndBank.cpp +++ b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperSndBank.cpp @@ -2,6 +2,7 @@ #include "Csv/CsvStream.h" #include "Game/T6/CommonT6.h" +#include "Game/T6/SoundConstantsT6.h" #include "ObjContainer/SoundBank/SoundBank.h" #include "Sound/WavWriter.h" #include "nlohmann/json.hpp" @@ -51,6 +52,7 @@ namespace "start_delay", "reverb_send", "duck", + "duck_group", "pan", "center_send", "envelop_min", @@ -118,147 +120,15 @@ namespace 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 CreateCurvesMap() { std::unordered_map result; - for (auto i = 0u; i < std::extent_v; i++) - result.emplace(T6::Common::SND_HashName(CURVES_ENUM[i].data()), CURVES_ENUM[i]); + for (auto i = 0u; i < std::extent_v; i++) + result.emplace(T6::Common::SND_HashName(SOUND_CURVES[i].data()), SOUND_CURVES[i]); return result; } const std::unordered_map 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 class AssetDumperSndBank::Internal @@ -344,7 +214,7 @@ class AssetDumperSndBank::Internal stream.WriteColumn(alias->name); // file - stream.WriteColumn(alias->assetFileName); + stream.WriteColumn(alias->assetFileName ? alias->assetFileName : ""); // template stream.WriteColumn(""); @@ -356,7 +226,7 @@ class AssetDumperSndBank::Internal stream.WriteColumn((alias->secondaryname && *alias->secondaryname) ? alias->secondaryname : ""); // group - stream.WriteColumn(GROUPS_ENUM[alias->flags.volumeGroup]); + stream.WriteColumn(SOUND_GROUPS[alias->flags.volumeGroup]); // vol_min stream.WriteColumn(std::to_string(alias->volMin)); @@ -377,28 +247,28 @@ class AssetDumperSndBank::Internal stream.WriteColumn(std::to_string(alias->distReverbMax)); // volume_falloff_curve - stream.WriteColumn(CURVES_ENUM[alias->flags.volumeFalloffCurve]); + stream.WriteColumn(SOUND_CURVES[alias->flags.volumeFalloffCurve]); // reverb_falloff_curve - stream.WriteColumn(CURVES_ENUM[alias->flags.reverbFalloffCurve]); + stream.WriteColumn(SOUND_CURVES[alias->flags.reverbFalloffCurve]); // volume_min_falloff_curve - stream.WriteColumn(CURVES_ENUM[alias->flags.volumeMinFalloffCurve]); + stream.WriteColumn(SOUND_CURVES[alias->flags.volumeMinFalloffCurve]); // reverb_min_falloff_curve" - stream.WriteColumn(CURVES_ENUM[alias->flags.reverbMinFalloffCurve]); + stream.WriteColumn(SOUND_CURVES[alias->flags.reverbMinFalloffCurve]); // limit_count stream.WriteColumn(std::to_string(alias->limitCount)); // limit_type - stream.WriteColumn(LIMIT_TYPES_ENUM[alias->flags.limitType]); + stream.WriteColumn(SOUND_LIMIT_TYPES[alias->flags.limitType]); // entity_limit_count stream.WriteColumn(std::to_string(alias->entityLimitCount)); // entity_limit_type - stream.WriteColumn(LIMIT_TYPES_ENUM[alias->flags.entityLimitType]); + stream.WriteColumn(SOUND_LIMIT_TYPES[alias->flags.entityLimitType]); // pitch_min stream.WriteColumn(std::to_string(alias->pitchMin)); @@ -425,13 +295,13 @@ class AssetDumperSndBank::Internal stream.WriteColumn(""); // type - stream.WriteColumn(LOAD_TYPES_ENUM[alias->flags.loadType]); + stream.WriteColumn(SOUND_LOAD_TYPES[alias->flags.loadType]); // loop stream.WriteColumn(alias->flags.looping == T6::SA_NON_LOOPING ? "nonlooping" : "looping"); // randomize_type - stream.WriteColumn(RANDOMIZE_TYPES_ENUM[alias->flags.randomizeType]); + stream.WriteColumn(SOUND_RANDOMIZE_TYPES[std::min(alias->flags.randomizeType, 3u)]); // probability", stream.WriteColumn(std::to_string(alias->probability)); @@ -445,6 +315,9 @@ class AssetDumperSndBank::Internal // duck", stream.WriteColumn(FindNameForDuck(alias->duck, bank)); + // duck_group", + stream.WriteColumn(SOUND_DUCK_GROUPS[alias->duckGroup]); + // pan", stream.WriteColumn(alias->flags.panType == SA_PAN_2D ? "2d" : "3d"); @@ -473,7 +346,7 @@ class AssetDumperSndBank::Internal stream.WriteColumn(alias->flags.distanceLpf ? "yes" : "no"); // move_type", - stream.WriteColumn(MOVE_TYPES_ENUM[alias->flags.fluxType]); + stream.WriteColumn(SOUND_MOVE_TYPES[alias->flags.fluxType]); // move_time", stream.WriteColumn(std::to_string(alias->fluxTime)); @@ -524,7 +397,7 @@ class AssetDumperSndBank::Internal stream.WriteColumn(alias->flags.stopOnDeath ? "yes" : "no"); // bus", - stream.WriteColumn(BUS_IDS_ENUM[alias->flags.busType]); + stream.WriteColumn(SOUND_BUS_IDS[alias->flags.busType]); // snapshot", stream.WriteColumn(""); @@ -647,13 +520,14 @@ class AssetDumperSndBank::Internal for (auto j = 0; j < aliasList.count; j++) { const auto& alias = aliasList.head[j]; + + WriteAliasToFile(csvStream, &alias, sndBank); + csvStream.NextRow(); + if (alias.assetId && alias.assetFileName && dumpedAssets.find(alias.assetId) == dumpedAssets.end()) { DumpSndAlias(alias); dumpedAssets.emplace(alias.assetId); - - WriteAliasToFile(csvStream, &alias, sndBank); - csvStream.NextRow(); } } } @@ -757,9 +631,9 @@ class AssetDumperSndBank::Internal for (auto i = 0u; i < 32u; i++) { values.push_back({ - {"duckGroup", DUCK_GROUPS_ENUM[i]}, - {"attenuation", duck.attenuation[i]}, - {"filter", duck.filter[i] } + {"duckGroup", SOUND_DUCK_GROUPS[i]}, + {"attenuation", duck.attenuation[i] }, + {"filter", duck.filter[i] } }); } diff --git a/src/ObjWriting/Sound/WavWriter.cpp b/src/ObjWriting/Sound/WavWriter.cpp index 6d0a5ae7..32b3e489 100644 --- a/src/ObjWriting/Sound/WavWriter.cpp +++ b/src/ObjWriting/Sound/WavWriter.cpp @@ -1,7 +1,5 @@ #include "WavWriter.h" -#include "Sound/WavTypes.h" - WavWriter::WavWriter(std::ostream& stream) : m_stream(stream) { diff --git a/src/ObjWriting/Sound/WavWriter.h b/src/ObjWriting/Sound/WavWriter.h index 4e7c2784..c3bda89f 100644 --- a/src/ObjWriting/Sound/WavWriter.h +++ b/src/ObjWriting/Sound/WavWriter.h @@ -1,12 +1,7 @@ #pragma once -#include +#include "Sound/WavTypes.h" -struct WavMetaData -{ - unsigned channelCount; - unsigned samplesPerSec; - unsigned bitsPerSample; -}; +#include class WavWriter { diff --git a/src/Utils/Utils/StringUtils.cpp b/src/Utils/Utils/StringUtils.cpp index bf8904da..d582fa1f 100644 --- a/src/Utils/Utils/StringUtils.cpp +++ b/src/Utils/Utils/StringUtils.cpp @@ -1,5 +1,6 @@ #include "StringUtils.h" +#include #include namespace utils @@ -100,4 +101,18 @@ namespace utils for (auto& c : str) c = static_cast(toupper(static_cast(c))); } + + std::vector StringSplit(const std::string& str, const char delim) + { + std::vector strings; + std::istringstream stream(str); + + std::string s; + while (std::getline(stream, s, delim)) + { + strings.emplace_back(std::move(s)); + } + + return strings; + } } // namespace utils diff --git a/src/Utils/Utils/StringUtils.h b/src/Utils/Utils/StringUtils.h index ca3f771d..4104c4f1 100644 --- a/src/Utils/Utils/StringUtils.h +++ b/src/Utils/Utils/StringUtils.h @@ -1,5 +1,6 @@ #pragma once #include +#include namespace utils { @@ -14,4 +15,6 @@ namespace utils void MakeStringLowerCase(std::string& str); void MakeStringUpperCase(std::string& str); + + std::vector StringSplit(const std::string& str, const char delim); } // namespace utils diff --git a/test/ObjCommonTests/Sound/FlacDecoderTest.cpp b/test/ObjCommonTests/Sound/FlacDecoderTest.cpp new file mode 100644 index 00000000..b7e6ecd4 --- /dev/null +++ b/test/ObjCommonTests/Sound/FlacDecoderTest.cpp @@ -0,0 +1,39 @@ +#include "Sound/FlacDecoder.h" + +#include +#include + +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