SoundBankWriter code

This commit is contained in:
Alex
2024-01-26 12:14:47 -05:00
parent acd9fa27fc
commit a020de6f80
15 changed files with 565 additions and 62 deletions

View File

@ -1,6 +1,7 @@
#include "AssetLoaderSoundBank.h"
#include "Csv/ParsedCsv.h"
#include "ObjContainer/SoundBank/SoundBankWriter.h"
#include "nlohmann/json.hpp"
#include "Game/T6/CommonT6.h"
@ -11,8 +12,54 @@
#include <Utils/StringUtils.h>
#include <cstring>
#include <iostream>
#include <fstream>
using namespace T6;
namespace fs = std::filesystem;
namespace
{
const std::string PREFIXES_TO_DROP[] {
"raw/",
"devraw/",
};
_NODISCARD std::string GetSoundFilePath(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;
}
}
void* AssetLoaderSoundBank::CreateEmptyAsset(const std::string& assetName, MemoryManager* memory)
{
@ -84,38 +131,44 @@ bool LoadSoundAlias(MemoryManager* memory, SndAlias* alias, const ParsedCsvRow&
alias->id = Common::SND_HashName(name.data());
alias->assetFileName = memory->Dup(aliasFileName.data());
alias->assetId = Common::SND_HashName(aliasFileName.data());
alias->secondaryname = memory->Dup(row.GetValue("secondary").data());
alias->subtitle = memory->Dup(row.GetValue("subtitle").data());
auto secondaryName = row.GetValue("secondary");
if (!secondaryName.empty())
alias->secondaryname = memory->Dup(secondaryName.data());
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.GetValueAs<uint16_t>("vol_min");
alias->volMax = row.GetValueAs<uint16_t>("vol_max");
alias->distMin = row.GetValueAs<uint16_t>("dist_min");
alias->distMax = row.GetValueAs<uint16_t>("dist_max");
alias->distReverbMax = row.GetValueAs<uint16_t>("dist_reverb_max");
alias->limitCount = row.GetValueAs<char>("limit_count");
alias->entityLimitCount = row.GetValueAs<char>("entity_limit_count");
alias->pitchMin = row.GetValueAs<uint16_t>("pitch_min");
alias->pitchMax = row.GetValueAs<uint16_t>("pitch_max");
alias->minPriority = row.GetValueAs<char>("min_priority");
alias->maxPriority = row.GetValueAs<char>("max_priority");
alias->minPriorityThreshold = row.GetValueAs<char>("min_priority_threshold");
alias->maxPriorityThreshold = row.GetValueAs<char>("max_priority_threshold");
alias->probability = row.GetValueAs<char>("probability");
alias->startDelay = row.GetValueAs<uint16_t>("start_delay");
alias->reverbSend = row.GetValueAs<uint16_t>("reverb_send");
alias->centerSend = row.GetValueAs<uint16_t>("center_send");
alias->envelopMin = row.GetValueAs<uint16_t>("envelop_min");
alias->envelopMax = row.GetValueAs<uint16_t>("envelop_max");
alias->envelopPercentage = row.GetValueAs<uint16_t>("envelop_percentage");
alias->occlusionLevel = row.GetValueAs<char>("occlusion_level");
alias->fluxTime = row.GetValueAs<uint16_t>("move_time");
alias->futzPatch = row.GetValueAs<unsigned int>("futz");
alias->contextType = row.GetValueAs<unsigned int>("context_type");
alias->contextValue = row.GetValueAs<unsigned int>("context_value");
alias->fadeIn = row.GetValueAs<int16_t>("fade_in");
alias->fadeOut = row.GetValueAs<int16_t>("fade_out");
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";
@ -309,22 +362,22 @@ bool LoadSoundRadverbs(MemoryManager* memory, SndBank* sndBank, const SearchPath
strncpy_s(sndBank->radverbs[i].name, name.data(), 32);
sndBank->radverbs[i].id = Common::SND_HashName(name.data());
sndBank->radverbs[i].smoothing = row.GetValueAs<float>("smoothing");
sndBank->radverbs[i].earlyTime = row.GetValueAs<float>("earlyTime");
sndBank->radverbs[i].lateTime = row.GetValueAs<float>("lateTime");
sndBank->radverbs[i].earlyGain = row.GetValueAs<float>("earlyGain");
sndBank->radverbs[i].lateGain = row.GetValueAs<float>("lateGain");
sndBank->radverbs[i].returnGain = row.GetValueAs<float>("returnGain");
sndBank->radverbs[i].earlyLpf = row.GetValueAs<float>("earlyLpf");
sndBank->radverbs[i].lateLpf = row.GetValueAs<float>("lateLpf");
sndBank->radverbs[i].inputLpf = row.GetValueAs<float>("inputLpf");
sndBank->radverbs[i].dampLpf = row.GetValueAs<float>("dampLpf");
sndBank->radverbs[i].wallReflect = row.GetValueAs<float>("wallReflect");
sndBank->radverbs[i].dryGain = row.GetValueAs<float>("dryGain");
sndBank->radverbs[i].earlySize = row.GetValueAs<float>("earlySize");
sndBank->radverbs[i].lateSize = row.GetValueAs<float>("lateSize");
sndBank->radverbs[i].diffusion = row.GetValueAs<float>("diffusion");
sndBank->radverbs[i].returnHighpass = row.GetValueAs<float>("returnHighpass");
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");
}
}
@ -416,6 +469,7 @@ bool AssetLoaderSoundBank::LoadFromRaw(
sndBank->name = memory->Dup(assetName.c_str());
auto sndBankLocalization = utils::StringSplit(assetName, '.');
// load the soundbank aliases
unsigned int loadedEntryCount = 0u, streamedEntryCount = 0u;
if (!LoadSoundAliasList(memory, sndBank, aliasFile, &loadedEntryCount, &streamedEntryCount))
return false;
@ -442,6 +496,9 @@ bool AssetLoaderSoundBank::LoadFromRaw(
}
}
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());
@ -454,6 +511,11 @@ bool AssetLoaderSoundBank::LoadFromRaw(
sndBank->loadedAssets.entryCount = loadedEntryCount;
sndBank->loadedAssets.entries = static_cast<SndAssetBankEntry*>(memory->Alloc(sizeof(SndAssetBankEntry) * loadedEntryCount));
memset(sndBank->loadedAssets.entries, 0, sizeof(SndAssetBankEntry) * loadedEntryCount);
const auto sablName = assetName + ".sabl";
sablStream = OpenSoundBankOutputFile(sablName);
if (sablStream)
sablWriter = SoundBankWriter::Create(sablName, *sablStream, searchPath);
}
if (streamedEntryCount > 0)
@ -461,6 +523,40 @@ bool AssetLoaderSoundBank::LoadFromRaw(
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)
sablWriter = SoundBankWriter::Create(sabsName, *sabsStream, searchPath);
}
// add aliases to the correct sound bank writer
for (auto i = 0u; i < sndBank->aliasCount; i++)
{
auto* aliasList = &sndBank->alias[i];
for (auto j = 0; j < aliasList->count; j++)
{
auto* alias = &aliasList->head[j];
if (sabsWriter && alias->flags.loadType == T6::SA_STREAMED)
sabsWriter->AddSound(GetSoundFilePath(alias), alias->assetId);
else if (sablWriter)
sablWriter->AddSound(GetSoundFilePath(alias), alias->assetId);
}
}
// write the output linked sound bank
if (sablWriter)
{
sablWriter->Write();
sablStream->close();
}
// write the output streamed sound bank
if (sabsWriter)
{
sabsWriter->Write();
sabsStream->close();
}
manager->AddAsset(ASSET_TYPE_SOUND, assetName, sndBank);

View File

@ -1,45 +0,0 @@
#pragma once
#include <cstdint>
class SoundBankConsts
{
SoundBankConsts() = default;
public:
static constexpr unsigned OFFSET_DATA_START = 0x800;
};
struct SoundAssetBankChecksum
{
char checksumBytes[16];
};
struct SoundAssetBankHeader
{
unsigned int magic; // + 0x0
unsigned int version; // + 0x4
unsigned int entrySize; // + 0x8
unsigned int checksumSize; // + 0xC
unsigned int dependencySize; // + 0x10
unsigned int entryCount; // + 0x14
unsigned int dependencyCount; // + 0x18
unsigned int pad32; // + 0x1C
int64_t fileSize; // + 0x20
int64_t entryOffset; // + 0x28
int64_t checksumOffset; // + 0x30
SoundAssetBankChecksum checksumChecksum; // + 0x38
char dependencies[512]; // + 0x48
};
struct SoundAssetBankEntry
{
unsigned int id;
unsigned int size;
unsigned int offset;
unsigned int frameCount;
unsigned char frameRateIndex;
unsigned char channelCount;
unsigned char looping;
unsigned char format;
};

View File

@ -0,0 +1,261 @@
#include "SoundBankWriter.h"
#include "Crypto.h"
#include "ObjContainer/SoundBank/SoundBankTypes.h"
#include "Sound/WavTypes.h"
#include "Utils/Alignment.h"
#include "Utils/FileUtils.h"
#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::SoundBankWriterImpl(const std::string& fileName, std::ostream& stream, ISearchPath* assetSearchPath)
: m_file_name(fileName),
m_stream(stream),
m_asset_search_path(assetSearchPath),
m_sounds(),
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) override
{
this->m_sounds.push_back(std::make_pair(soundFilePath, soundId));
}
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 & 0xF) != 0)
Pad(0x10 - (m_current_offset & 0xF));
}
void WriteHeader()
{
GoTo(0);
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.first;
const auto soundId = sound.second;
size_t soundSize = -1;
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));
auto frameCount = soundSize / (header.formatChunk.nChannels * (header.formatChunk.wBitsPerSample / 8));
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),
0,
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 + ".wav");
if (flacFile.IsOpen())
{
soundSize = static_cast<size_t>(flacFile.m_length);
SoundAssetBankEntry entry{
soundId,
soundSize,
static_cast<size_t>(m_current_offset),
0,
0,
0,
0,
8,
};
m_entries.push_back(entry);
soundData = std::make_unique<char[]>(soundSize);
flacFile.m_stream->read(soundData.get(), soundSize);
}
else
{
std::cerr << "Unable to find a compatible file for sound " << soundFilePath << std::endl;
return false;
}
}
// 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() override
{
WriteEntries();
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;
}
return true;
}
private:
std::string m_file_name;
std::ostream& m_stream;
ISearchPath* m_asset_search_path;
std::vector<std::pair<std::string, unsigned int>> 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 <memory>
#include <ostream>
#include <filesystem>
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) = 0;
virtual bool Write() = 0;
static std::unique_ptr<SoundBankWriter> Create(const std::string& fileName, std::ostream& stream, ISearchPath* assetSearchPath);
static std::filesystem::path OutputPath;
};