OpenAssetTools/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperSndBank.cpp
2024-10-07 12:33:01 -07:00

928 lines
29 KiB
C++

#include "AssetDumperSndBank.h"
#include "Csv/CsvStream.h"
#include "Game/T6/CommonT6.h"
#include "Game/T6/GameAssetPoolT6.h"
#include "Game/T6/GameT6.h"
#include "Game/T6/SoundConstantsT6.h"
#include "ObjContainer/SoundBank/SoundBank.h"
#include "Sound/WavWriter.h"
#include "nlohmann/json.hpp"
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <format>
#include <fstream>
#include <sstream>
#include <unordered_set>
using namespace T6;
namespace fs = std::filesystem;
namespace
{
const std::string ALIAS_HEADERS[]{
"Name",
"FileSource",
"Secondary",
"Storage",
"Bus",
"VolumeGroup",
"DuckGroup",
"Duck",
"ReverbSend",
"CenterSend",
"VolMin",
"VolMax",
"DistMin",
"DistMaxDry",
"DistMaxWet",
"DryMinCurve",
"DryMaxCurve",
"WetMinCurve",
"WetMaxCurve",
"LimitCount",
"EntityLimitCount",
"LimitType",
"EntityLimitType",
"PitchMin",
"PitchMax",
"PriorityMin",
"PriorityMax",
"PriorityThresholdMin",
"PriorityThresholdMax",
"PanType",
"Pan",
"Looping",
"RandomizeType",
"Probability",
"StartDelay",
"EnvelopMin",
"EnvelopMax",
"EnvelopPercent",
"OcclusionLevel",
"IsBig",
"DistanceLpf",
"FluxType",
"FluxTime",
"Subtitle",
"Doppler",
"ContextType",
"ContextValue",
"Timescale",
"IsMusic",
"IsCinematic",
"FadeIn",
"FadeOut",
"Pauseable",
"StopOnEntDeath",
"StopOnPlay",
"DopplerScale",
"FutzPatch",
"VoiceLimit",
"IgnoreMaxDist",
"NeverPlayTwice",
};
const std::string REVERB_HEADERS[]{
"name",
"smoothing",
"earlyTime",
"lateTime",
"earlyGain",
"lateGain",
"returnGain",
"earlyLpf",
"lateLpf",
"inputLpf",
"dampLpf",
"wallReflect",
"dryGain",
"earlySize",
"lateSize",
"diffusion",
"returnHighpass",
};
const std::string PREFIXES_TO_DROP[]{
"raw/",
"devraw/",
};
constexpr size_t FRAME_RATE_FOR_INDEX[]{
8000,
12000,
16000,
24000,
32000,
44100,
48000,
96000,
192000,
};
constexpr const char* KNOWN_CONTEXT_TYPES[]{
"",
"plr_stance",
"grass",
"f35",
"ringoff_plr",
"mature",
};
constexpr const char* KNOWN_CONTEXT_VALUES[]{
"",
"stand",
"crouch",
"prone",
"no_grass",
"in_grass",
"interior",
"exterior",
"outdoor",
"indoor",
"safe",
"explicit",
};
constexpr const char* KNOWN_FUTZ_IDS[]{
"",
"bfutz",
"default",
"defaultbkp",
"dlc_res_1",
"dlc_res_2",
"dlc_res_3",
"dlc_res_4",
"dlc_res_5",
"dlc_res_6",
"dlc_res_7",
"dlc_res_8",
"good_1",
"jet_wing_helmet",
"jet_wing_helmet_flying",
"mpl_agr_pov",
"mpl_chopper_pov",
"mpl_quad_pov",
"mpl_reaper_pov",
"no_gfutz",
"spl_asd_pov",
"spl_bigdog_pov",
"spl_heli_future",
"spl_quad_pov",
"spl_spiderbot_pov",
"spl_spymic",
"spl_tow_missile",
"spl_turret",
"spl_war_command",
"test_1",
"test_2",
"tueyeckert",
};
std::unordered_map<unsigned int, std::string> CreateSoundHashMap(const char* const* values, const unsigned valueCount)
{
std::unordered_map<unsigned int, std::string> result;
for (auto i = 0u; i < valueCount; i++)
result.emplace(Common::SND_HashName(values[i]), values[i]);
return result;
}
const auto CURVES_MAP = CreateSoundHashMap(SOUND_CURVES, std::extent_v<decltype(SOUND_CURVES)>);
const auto CONTEXT_TYPES_MAP = CreateSoundHashMap(KNOWN_CONTEXT_TYPES, std::extent_v<decltype(KNOWN_CONTEXT_TYPES)>);
const auto CONTEXT_VALUES_MAP = CreateSoundHashMap(KNOWN_CONTEXT_VALUES, std::extent_v<decltype(KNOWN_CONTEXT_VALUES)>);
const auto FUTZ_IDS_MAP = CreateSoundHashMap(KNOWN_FUTZ_IDS, std::extent_v<decltype(KNOWN_FUTZ_IDS)>);
class LoadedSoundBankHashes
{
public:
void Initialize()
{
for (const auto& zone : g_GameT6.GetZones())
{
auto& sndBankPool = *dynamic_cast<GameAssetPoolT6*>(zone->m_pools.get())->m_sound_bank;
for (auto* entry : sndBankPool)
{
const auto& sndBank = *entry->Asset();
m_alias_names.reserve(m_alias_names.size() + sndBank.aliasCount);
for (auto aliasIndex = 0u; aliasIndex < sndBank.aliasCount; aliasIndex++)
{
const auto& alias = sndBank.alias[aliasIndex];
m_alias_names.emplace(alias.id, alias.name);
}
m_duck_names.reserve(m_duck_names.size() + sndBank.duckCount);
for (auto duckIndex = 0u; duckIndex < sndBank.duckCount; duckIndex++)
{
const auto& duck = sndBank.ducks[duckIndex];
m_duck_names.emplace(duck.id, duck.name);
}
}
}
}
[[nodiscard]] std::optional<const char*> GetAliasName(const unsigned hash) const
{
if (hash == 0)
return "";
const auto knownAliasHash = m_alias_names.find(hash);
if (knownAliasHash != m_alias_names.end())
return knownAliasHash->second;
return std::nullopt;
}
[[nodiscard]] std::optional<const char*> GetDuckName(const unsigned hash) const
{
if (hash == 0)
return "";
const auto knownDuckHash = m_duck_names.find(hash);
if (knownDuckHash != m_duck_names.end())
return knownDuckHash->second;
return std::nullopt;
}
private:
std::unordered_map<unsigned, const char*> m_alias_names;
std::unordered_map<unsigned, const char*> m_duck_names;
};
[[nodiscard]] std::string GetAssetFilename(const AssetDumpingContext& context, std::string outputFileName, const std::string& extension)
{
fs::path assetPath(context.m_base_path);
std::ranges::replace(outputFileName, '\\', '/');
for (const auto& droppedPrefix : PREFIXES_TO_DROP)
{
if (outputFileName.rfind(droppedPrefix, 0) != std::string::npos)
{
outputFileName.erase(0, droppedPrefix.size());
break;
}
}
assetPath.append(outputFileName);
if (!extension.empty())
assetPath.concat(extension);
return assetPath.string();
}
std::unique_ptr<std::ostream> OpenAssetOutputFile(const AssetDumpingContext& context, const std::string& outputFileName, const std::string& extension)
{
fs::path assetPath(GetAssetFilename(context, outputFileName, extension));
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 WriteAliasFileHeader(CsvOutputStream& stream)
{
for (const auto& headerField : ALIAS_HEADERS)
{
stream.WriteColumn(headerField);
}
stream.NextRow();
}
void WriteReverbFileHeader(CsvOutputStream& stream)
{
for (const auto& headerField : REVERB_HEADERS)
{
stream.WriteColumn(headerField);
}
stream.NextRow();
}
const char* ExtensionForSndFormat(const snd_asset_format format)
{
switch (format)
{
case SND_ASSET_FORMAT_PCMS16:
return ".wav";
case SND_ASSET_FORMAT_FLAC:
return ".flac";
default:
assert(false);
return "";
}
}
void WriteColumnString(CsvOutputStream& stream, const std::string& stringValue)
{
stream.WriteColumn(stringValue);
}
void WriteColumnString(CsvOutputStream& stream, const char* stringValue)
{
stream.WriteColumn(stringValue ? std::string(stringValue) : std::string());
}
template<typename T> void WriteColumnIntegral(CsvOutputStream& stream, const T value)
{
stream.WriteColumn(std::format("{}", value));
}
void WriteColumnEnumWithSize(CsvOutputStream& stream, const unsigned value, const char* const* enumValues, const size_t enumValueCount)
{
assert(value < enumValueCount);
stream.WriteColumn(value < enumValueCount ? enumValues[value] : "");
}
template<size_t Size> void WriteColumnEnum(CsvOutputStream& stream, const unsigned value, const char* const (&enumValues)[Size])
{
WriteColumnEnumWithSize(stream, value, enumValues, Size);
}
void WriteColumnEnumFlagsWithSize(CsvOutputStream& stream, const unsigned value, const char* const* enumValues, const size_t enumValueCount)
{
assert(enumValueCount <= 32u);
std::ostringstream ss;
auto first = true;
for (auto i = 0u; i < enumValueCount; i++)
{
const auto flagValue = 1u << i;
if (value & flagValue)
{
if (first)
first = false;
else
ss << ' ';
ss << enumValues[i];
}
}
stream.WriteColumn(ss.str());
}
template<size_t Size> void WriteColumnEnumFlags(CsvOutputStream& stream, const unsigned value, const char* const (&enumValues)[Size])
{
WriteColumnEnumFlagsWithSize(stream, value, enumValues, Size);
}
void WriteColumnVolumeLinear(CsvOutputStream& stream, const uint16_t value)
{
const auto linear = static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
const auto dbSpl = std::clamp(T6::Common::LinearToDbspl(linear), 0.0f, 100.0f);
float dbSplRound;
for (auto i = 0; i <= 4; i++)
{
dbSplRound = std::stof(std::format("{:.{}f}", dbSpl, i));
const auto dbSplRoundToValue =
static_cast<uint16_t>(T6::Common::DbsplToLinear(dbSplRound) * static_cast<float>(std::numeric_limits<uint16_t>::max()));
if (dbSplRoundToValue == value)
{
break;
}
}
stream.WriteColumn(std::format("{}", dbSplRound));
}
void WriteColumnPitchHertz(CsvOutputStream& stream, const uint16_t value)
{
const auto hertz = static_cast<float>(value) / static_cast<float>(std::numeric_limits<int16_t>::max());
const auto cents = std::clamp(T6::Common::HertzToCents(hertz), -2400.0f, 1200.0f);
float centsRound;
for (auto i = 0; i <= 4; i++)
{
centsRound = std::stof(std::format("{:.{}f}", cents, i));
const auto centsRoundToValue =
static_cast<uint16_t>(T6::Common::CentsToHertz(centsRound) * static_cast<float>(std::numeric_limits<int16_t>::max()));
if (centsRoundToValue == value)
{
break;
}
}
stream.WriteColumn(std::format("{}", centsRound));
}
void WriteColumnNormByte(CsvOutputStream& stream, const uint8_t value)
{
const auto normValue = static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint8_t>::max());
float normValueRound;
for (auto i = 0; i <= 4; i++)
{
normValueRound = std::stof(std::format("{:.{}f}", normValue, i));
const auto normValueRoundToValue = static_cast<uint8_t>(normValueRound * static_cast<float>(std::numeric_limits<uint8_t>::max()));
if (normValueRoundToValue == value)
{
break;
}
}
stream.WriteColumn(std::format("{}", normValueRound));
}
void WriteColumnWithKnownHashes(CsvOutputStream& stream, const std::unordered_map<unsigned, std::string>& knownValues, const unsigned value)
{
const auto knownValue = knownValues.find(value);
if (knownValue != knownValues.end())
stream.WriteColumn(knownValue->second);
else
stream.WriteColumn(std::format("@{:x}", value));
}
void WriteColumnWithAliasHash(CsvOutputStream& stream, const LoadedSoundBankHashes& hashes, const unsigned value)
{
const auto name = hashes.GetAliasName(value);
if (name)
stream.WriteColumn(*name);
else
stream.WriteColumn(std::format("@{:x}", value));
}
void WriteColumnWithDuckHash(CsvOutputStream& stream, const LoadedSoundBankHashes& hashes, const unsigned value)
{
const auto name = hashes.GetDuckName(value);
if (name)
stream.WriteColumn(*name);
else
stream.WriteColumn(std::format("@{:x}", value));
}
void
WriteAliasToFile(CsvOutputStream& stream, const SndAlias& alias, const std::optional<snd_asset_format> maybeFormat, const LoadedSoundBankHashes& hashes)
{
// Name
WriteColumnString(stream, alias.name);
// FileSource
const auto* extension = maybeFormat ? ExtensionForSndFormat(*maybeFormat) : "";
WriteColumnString(stream, alias.assetFileName ? std::format("{}{}", alias.assetFileName, extension) : "");
// Secondary
WriteColumnString(stream, alias.secondaryName);
// Storage
WriteColumnEnum(stream, alias.flags.loadType, SOUND_LOAD_TYPES);
// Bus
WriteColumnEnum(stream, alias.flags.busType, SOUND_BUS_IDS);
// VolumeGroup
WriteColumnEnum(stream, alias.flags.volumeGroup, SOUND_GROUPS);
// DuckGroup
WriteColumnEnum(stream, alias.duckGroup, SOUND_DUCK_GROUPS);
// Duck
WriteColumnWithDuckHash(stream, hashes, alias.duck);
// ReverbSend
WriteColumnVolumeLinear(stream, alias.reverbSend);
// CenterSend
WriteColumnVolumeLinear(stream, alias.centerSend);
// VolMin
WriteColumnVolumeLinear(stream, alias.volMin);
// VolMax
WriteColumnVolumeLinear(stream, alias.volMax);
// DistMin
WriteColumnIntegral(stream, alias.distMin);
// DistMaxDry
WriteColumnIntegral(stream, alias.distMax);
// DistMaxWet
WriteColumnIntegral(stream, alias.distReverbMax);
// DryMinCurve
WriteColumnEnum(stream, alias.flags.volumeMinFalloffCurve, SOUND_CURVES);
// DryMaxCurve
WriteColumnEnum(stream, alias.flags.volumeFalloffCurve, SOUND_CURVES);
// WetMinCurve
WriteColumnEnum(stream, alias.flags.reverbMinFalloffCurve, SOUND_CURVES);
// WetMaxCurve
WriteColumnEnum(stream, alias.flags.reverbFalloffCurve, SOUND_CURVES);
// LimitCount
WriteColumnIntegral(stream, alias.limitCount);
// EntityLimitCount
WriteColumnIntegral(stream, alias.entityLimitCount);
// LimitType
WriteColumnEnum(stream, alias.flags.limitType, SOUND_LIMIT_TYPES);
// EntityLimitType
WriteColumnEnum(stream, alias.flags.entityLimitType, SOUND_LIMIT_TYPES);
// PitchMin
WriteColumnPitchHertz(stream, alias.pitchMin);
// PitchMax
WriteColumnPitchHertz(stream, alias.pitchMax);
// PriorityMin
WriteColumnIntegral(stream, alias.minPriority);
// PriorityMax
WriteColumnIntegral(stream, alias.maxPriority);
// PriorityThresholdMin
WriteColumnNormByte(stream, alias.minPriorityThreshold);
// PriorityThresholdMax
WriteColumnNormByte(stream, alias.maxPriorityThreshold);
// PanType
WriteColumnEnum(stream, alias.flags.panType, SOUND_PAN_TYPES);
// Pan
WriteColumnEnum(stream, alias.pan, SOUND_PANS);
// Looping
WriteColumnEnum(stream, alias.flags.looping, SOUND_LOOP_TYPES);
// RandomizeType
WriteColumnEnumFlags(stream, alias.flags.randomizeType, SOUND_RANDOMIZE_TYPES);
// Probability
WriteColumnNormByte(stream, alias.probability);
// StartDelay
WriteColumnIntegral(stream, alias.startDelay);
// EnvelopMin
WriteColumnIntegral(stream, alias.envelopMin);
// EnvelopMax
WriteColumnIntegral(stream, alias.envelopMax);
// EnvelopPercent
WriteColumnVolumeLinear(stream, alias.envelopPercentage);
// OcclusionLevel
WriteColumnNormByte(stream, alias.occlusionLevel);
// IsBig
WriteColumnEnum(stream, alias.flags.isBig, SOUND_NO_YES);
// DistanceLpf
WriteColumnEnum(stream, alias.flags.distanceLpf, SOUND_NO_YES);
// FluxType
WriteColumnEnum(stream, alias.flags.fluxType, SOUND_MOVE_TYPES);
// FluxTime
WriteColumnIntegral(stream, alias.fluxTime);
// Subtitle
WriteColumnString(stream, alias.subtitle);
// Doppler
WriteColumnEnum(stream, alias.flags.doppler, SOUND_NO_YES);
// ContextType
WriteColumnWithKnownHashes(stream, CONTEXT_TYPES_MAP, alias.contextType);
// ContextValue
WriteColumnWithKnownHashes(stream, CONTEXT_VALUES_MAP, alias.contextValue);
// Timescale
WriteColumnEnum(stream, alias.flags.timescale, SOUND_NO_YES);
// IsMusic
WriteColumnEnum(stream, alias.flags.isMusic, SOUND_NO_YES);
// IsCinematic
WriteColumnEnum(stream, alias.flags.isCinematic, SOUND_NO_YES);
// FadeIn
WriteColumnIntegral(stream, alias.fadeIn);
// FadeOut
WriteColumnIntegral(stream, alias.fadeOut);
// Pauseable
WriteColumnEnum(stream, alias.flags.pauseable, SOUND_NO_YES);
// StopOnEntDeath
WriteColumnEnum(stream, alias.flags.stopOnEntDeath, SOUND_NO_YES);
// StopOnPlay
WriteColumnWithAliasHash(stream, hashes, alias.stopOnPlay);
// DopplerScale
WriteColumnIntegral(stream, alias.dopplerScale);
// FutzPatch
WriteColumnWithKnownHashes(stream, FUTZ_IDS_MAP, alias.futzPatch);
// VoiceLimit
WriteColumnEnum(stream, alias.flags.voiceLimit, SOUND_NO_YES);
// IgnoreMaxDist
WriteColumnEnum(stream, alias.flags.ignoreMaxDist, SOUND_NO_YES);
// NeverPlayTwice
WriteColumnEnum(stream, alias.flags.neverPlayTwice, SOUND_NO_YES);
}
SoundBankEntryInputStream FindSoundDataInSoundBanks(const unsigned assetId)
{
for (const auto* soundBank : SoundBank::Repository)
{
auto soundFile = soundBank->GetEntryStream(assetId);
if (soundFile.IsOpen())
return soundFile;
}
return {};
}
void DumpSoundFilePcm(const AssetDumpingContext& context,
const char* assetFileName,
const SoundBankEntryInputStream& soundFile,
const unsigned bitsPerSample)
{
const auto outFile = OpenAssetOutputFile(context, assetFileName, ".wav");
if (!outFile)
{
std::cerr << std::format("Failed to open sound output file: \"{}\"\n", assetFileName);
return;
}
const WavWriter writer(*outFile);
if (soundFile.m_entry.frameRateIndex >= std::extent_v<decltype(FRAME_RATE_FOR_INDEX)>)
return;
const WavMetaData metaData{soundFile.m_entry.channelCount, FRAME_RATE_FOR_INDEX[soundFile.m_entry.frameRateIndex], bitsPerSample};
writer.WritePcmHeader(metaData, soundFile.m_entry.size);
while (!soundFile.m_stream->eof())
{
char buffer[2048];
soundFile.m_stream->read(buffer, sizeof(buffer));
const auto readSize = soundFile.m_stream->gcount();
outFile->write(buffer, readSize);
}
}
void DumpSoundFilePassthrough(const AssetDumpingContext& context,
const char* assetFileName,
const SoundBankEntryInputStream& soundFile,
const std::string& extension)
{
const auto outFile = OpenAssetOutputFile(context, assetFileName, extension);
if (!outFile)
{
std::cerr << std::format("Failed to open sound output file: \"{}\"\n", assetFileName);
return;
}
while (!soundFile.m_stream->eof())
{
char buffer[2048];
soundFile.m_stream->read(buffer, sizeof(buffer));
const auto readSize = soundFile.m_stream->gcount();
outFile->write(buffer, readSize);
}
}
[[nodiscard]] std::optional<snd_asset_format> DumpSndAlias(const AssetDumpingContext& context, const SndAlias& alias)
{
const auto soundFile = FindSoundDataInSoundBanks(alias.assetId);
if (soundFile.IsOpen())
{
const auto format = static_cast<snd_asset_format>(soundFile.m_entry.format);
switch (format)
{
case SND_ASSET_FORMAT_PCMS16:
DumpSoundFilePcm(context, alias.assetFileName, soundFile, 16u);
break;
case SND_ASSET_FORMAT_FLAC:
DumpSoundFilePassthrough(context, alias.assetFileName, soundFile, ".flac");
break;
case SND_ASSET_FORMAT_PCMS24:
case SND_ASSET_FORMAT_PCMS32:
case SND_ASSET_FORMAT_IEEE:
case SND_ASSET_FORMAT_XMA4:
case SND_ASSET_FORMAT_MSADPCM:
case SND_ASSET_FORMAT_WMA:
case SND_ASSET_FORMAT_WIIUADPCM:
case SND_ASSET_FORMAT_MPC:
std::cerr << std::format("Cannot dump sound (Unknown sound format {}): \"{}\"\n", static_cast<unsigned>(format), alias.assetFileName);
break;
default:
assert(false);
std::cerr << std::format("Cannot dump sound (Unknown sound format {}): \"{}\"\n", static_cast<unsigned>(format), alias.assetFileName);
break;
}
return format;
}
std::cerr << std::format("Could not find data for sound \"{}\"\n", alias.assetFileName);
return {};
}
void DumpSndBankAliases(const AssetDumpingContext& context, const LoadedSoundBankHashes& hashes, const SndBank& sndBank)
{
std::unordered_map<unsigned int, std::optional<snd_asset_format>> dumpedAssets;
const auto outFile = OpenAssetOutputFile(context, std::format("soundbank/{}.aliases", sndBank.name), ".csv");
if (!outFile)
{
std::cerr << std::format("Failed to open sound alias output file: \"\"\n", sndBank.name);
return;
}
CsvOutputStream csvStream(*outFile);
WriteAliasFileHeader(csvStream);
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];
std::optional<snd_asset_format> maybeFormat;
if (alias.assetId && alias.assetFileName)
{
const auto previouslyDeterminedFormat = dumpedAssets.find(alias.assetId);
if (previouslyDeterminedFormat == dumpedAssets.end())
{
maybeFormat = DumpSndAlias(context, alias);
dumpedAssets[alias.assetId] = maybeFormat;
}
else
{
maybeFormat = previouslyDeterminedFormat->second;
}
}
WriteAliasToFile(csvStream, alias, maybeFormat, hashes);
csvStream.NextRow();
}
}
}
void DumpSoundRadverb(const AssetDumpingContext& context, const SndBank& sndBank)
{
if (sndBank.radverbCount <= 0)
return;
const auto outFile = OpenAssetOutputFile(context, std::format("soundbank/{}.reverbs", sndBank.name), ".csv");
if (!outFile)
{
std::cerr << std::format("Failed to open sound reverb output file: \"{}\"\n", sndBank.name);
return;
}
CsvOutputStream csvStream(*outFile);
WriteReverbFileHeader(csvStream);
for (auto i = 0u; i < sndBank.radverbCount; i++)
{
const auto& reverb = sndBank.radverbs[i];
csvStream.WriteColumn(reverb.name);
csvStream.WriteColumn(std::to_string(reverb.smoothing));
csvStream.WriteColumn(std::to_string(reverb.earlyTime));
csvStream.WriteColumn(std::to_string(reverb.lateTime));
csvStream.WriteColumn(std::to_string(reverb.earlyGain));
csvStream.WriteColumn(std::to_string(reverb.lateGain));
csvStream.WriteColumn(std::to_string(reverb.returnGain));
csvStream.WriteColumn(std::to_string(reverb.earlyLpf));
csvStream.WriteColumn(std::to_string(reverb.lateLpf));
csvStream.WriteColumn(std::to_string(reverb.inputLpf));
csvStream.WriteColumn(std::to_string(reverb.dampLpf));
csvStream.WriteColumn(std::to_string(reverb.wallReflect));
csvStream.WriteColumn(std::to_string(reverb.dryGain));
csvStream.WriteColumn(std::to_string(reverb.earlySize));
csvStream.WriteColumn(std::to_string(reverb.lateSize));
csvStream.WriteColumn(std::to_string(reverb.diffusion));
csvStream.WriteColumn(std::to_string(reverb.returnHighpass));
csvStream.NextRow();
}
}
void DumpSoundDucks(const AssetDumpingContext& context, const SndBank& sndBank)
{
if (sndBank.duckCount <= 0)
return;
const auto outFile = OpenAssetOutputFile(context, std::format("soundbank/{}.ducklist", sndBank.name), ".csv");
if (!outFile)
{
std::cerr << std::format("Failed to open sound reverb output file: \"{}\"\n", sndBank.name);
return;
}
CsvOutputStream csvStream(*outFile);
csvStream.WriteColumn("name");
csvStream.NextRow();
for (auto i = 0u; i < sndBank.duckCount; i++)
{
const auto& duck = sndBank.ducks[i];
csvStream.WriteColumn(duck.name);
csvStream.NextRow();
const auto duckFile = OpenAssetOutputFile(context, std::format("soundbank/ducks/{}", duck.name), ".duk");
if (!outFile)
{
std::cerr << std::format("Failed to open sound duck output file: \"{}\"\n", duck.name);
return;
}
nlohmann::json duckObj{};
duckObj["fadeIn"] = duck.fadeIn;
duckObj["fadeOut"] = duck.fadeOut;
duckObj["startDelay"] = duck.startDelay;
duckObj["distance"] = duck.distance;
duckObj["length"] = duck.length;
duckObj["fadeInCurveId"] = duck.fadeInCurve;
duckObj["fadeOutCurveId"] = duck.fadeOutCurve;
duckObj["updateWhilePaused"] = duck.updateWhilePaused;
auto fadeInItr = CURVES_MAP.find(duck.fadeInCurve);
if (fadeInItr != CURVES_MAP.end())
{
duckObj["fadeInCurve"] = fadeInItr->second;
}
auto fadeOutItr = CURVES_MAP.find(duck.fadeOutCurve);
if (fadeOutItr != CURVES_MAP.end())
{
duckObj["fadeOutCurve"] = fadeOutItr->second;
}
auto values = std::vector<nlohmann::json>{};
for (auto j = 0u; j < 32u; j++)
{
values.push_back({
{"duckGroup", SOUND_DUCK_GROUPS[j]},
{"attenuation", duck.attenuation[j] },
{"filter", duck.filter[j] }
});
}
duckObj["values"] = values;
*duckFile << duckObj.dump(4) << "\n";
}
}
void DumpSndBank(const AssetDumpingContext& context, const LoadedSoundBankHashes& hashes, const XAssetInfo<SndBank>& sndBankInfo)
{
const auto* sndBank = sndBankInfo.Asset();
DumpSndBankAliases(context, hashes, *sndBank);
DumpSoundRadverb(context, *sndBank);
DumpSoundDucks(context, *sndBank);
}
} // namespace
void AssetDumperSndBank::DumpPool(AssetDumpingContext& context, AssetPool<SndBank>* pool)
{
LoadedSoundBankHashes soundBankHashes;
soundBankHashes.Initialize();
for (const auto* assetInfo : *pool)
{
if (!assetInfo->m_name.empty() && assetInfo->m_name[0] == ',')
continue;
DumpSndBank(context, soundBankHashes, *assetInfo);
}
}