mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-29 15:23:01 -04:00
Mod api audio_res service for replacing audio samples (waves)
More things planned for the future, this is a basic prototype and far from finished. Still needs lots of work
This commit is contained in:
@@ -90,6 +90,7 @@ option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support"
|
||||
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
|
||||
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
|
||||
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
|
||||
option(DUSK_ENABLE_OPUS "Enable loading Opus audio files for mods" ON)
|
||||
|
||||
# Edit & Continue
|
||||
if (MSVC)
|
||||
@@ -219,6 +220,25 @@ FetchContent_Declare(miniz
|
||||
)
|
||||
|
||||
set(_fetch_content_deps cxxopts json miniz)
|
||||
|
||||
if (DUSK_ENABLE_OPUS)
|
||||
message(STATUS "dusklight: Fetching opusfile")
|
||||
|
||||
# Opusfile options
|
||||
SET(OP_DISABLE_HTTP ON CACHE BOOL "" FORCE)
|
||||
SET(OP_DISABLE_DOCS ON CACHE BOOL "" FORCE)
|
||||
SET(OP_DISABLE_EXAMPLES ON CACHE BOOL "" FORCE)
|
||||
|
||||
message(STATUS "dusklight: Fetching opusfile")
|
||||
FetchContent_Declare(
|
||||
opusfile
|
||||
GIT_REPOSITORY https://github.com/xiph/opusfile.git
|
||||
GIT_TAG 6dfd29e7adb87f2e193575fc3fa88cbf1a0b27df
|
||||
)
|
||||
|
||||
list(APPEND _fetch_content_deps opusfile)
|
||||
endif ()
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
message(STATUS "dusklight: Fetching funchook")
|
||||
# cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a
|
||||
@@ -311,6 +331,10 @@ set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::p
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
list(APPEND GAME_LIBS funchook-static)
|
||||
endif ()
|
||||
if (DUSK_ENABLE_OPUS)
|
||||
list(APPEND GAME_LIBS OpusFile::opusfile)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_OPUS=1)
|
||||
endif ()
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
list(APPEND GAME_LIBS sentry)
|
||||
@@ -568,6 +592,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
|
||||
add_subdirectory(mods/template_mod)
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
add_subdirectory(mods/audio_mod)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
|
||||
@@ -1483,6 +1483,10 @@ set(DUSK_FILES
|
||||
src/dusk/mods/log_buffer.hpp
|
||||
src/dusk/mods/manifest.cpp
|
||||
src/dusk/mods/manifest.hpp
|
||||
src/dusk/mods/svc/audio_res/audio_res.hpp
|
||||
src/dusk/mods/svc/audio_res/audio_res.cpp
|
||||
src/dusk/mods/svc/audio_res/wave.cpp
|
||||
src/dusk/mods/svc/audio_res/opus.cpp
|
||||
src/dusk/mods/svc/camera.cpp
|
||||
src/dusk/mods/svc/config.cpp
|
||||
src/dusk/mods/svc/config.hpp
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#include "JSystem/JSystem.h" // IWYU pragma: keep
|
||||
|
||||
#include "JSystem/JAudio2/JASBank.h"
|
||||
|
||||
#include "dusk/mods/svc/audio_res/audio_res.hpp"
|
||||
#include "JSystem/JAudio2/JASAiCtrl.h"
|
||||
#include "JSystem/JAudio2/JASBasicInst.h"
|
||||
#include "JSystem/JAudio2/JASBasicWaveBank.h"
|
||||
#include "JSystem/JAudio2/JASChannel.h"
|
||||
|
||||
using namespace dusk::mods::svc::audio_res;
|
||||
|
||||
// NONMATCHING JASPoolAllocObject_MultiThreaded<_> locations
|
||||
JASChannel* JASBank::noteOn(JASBank const* param_0, int param_1, u8 param_2, u8 param_3, u16 param_4,
|
||||
void (*param_5)(u32, JASChannel*, JASDsp::TChannel*, void*),
|
||||
@@ -28,6 +32,13 @@ JASChannel* JASBank::noteOn(JASBank const* param_0, int param_1, u8 param_2, u8
|
||||
if (!waveHandle) {
|
||||
return NULL;
|
||||
}
|
||||
#if TARGET_PC
|
||||
auto const found_replacement = s_replacements.find(AudioWaveKey(SoundEffects, stack_60.field_0x1a));
|
||||
if (found_replacement != s_replacements.end()) {
|
||||
waveHandle = found_replacement->second.get();
|
||||
}
|
||||
#endif
|
||||
|
||||
const JASWaveInfo* waveInfo = waveHandle->getWaveInfo();
|
||||
if (!waveInfo) {
|
||||
return NULL;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#define AUDIO_RES_SERVICE_ID "dev.twilitrealm.dusklight.audio_res"
|
||||
#define AUDIO_RES_SERVICE_MAJOR 1u
|
||||
#define AUDIO_RES_SERVICE_MINOR 0u
|
||||
|
||||
#define AUDIO_RES_DEFAULT_KEY 0x3C;
|
||||
|
||||
typedef enum AudioWaveBank : uint8_t {
|
||||
SoundEffects = 0,
|
||||
MusicSamples = 1,
|
||||
} AudioWaveBank;
|
||||
|
||||
typedef enum AudioWaveFormat : uint8_t {
|
||||
Adpcm4 = 0,
|
||||
Adpcm2 = 1,
|
||||
Pcm8 = 2,
|
||||
Pcm16 = 3,
|
||||
} AudioWaveFormat;
|
||||
|
||||
typedef uint64_t AudioWaveHandle;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
typedef struct AudioRawWave {
|
||||
AudioWaveFormat format;
|
||||
float sample_rate;
|
||||
int16_t sample_value_last;
|
||||
int16_t sample_value_penult;
|
||||
} AudioRawWave;
|
||||
|
||||
typedef struct AudioWaveInfo {
|
||||
uint8_t base_key;
|
||||
bool loop;
|
||||
uint32_t loop_start_sample;
|
||||
uint32_t loop_end_sample;
|
||||
|
||||
AudioRawWave const* raw_wave;
|
||||
} AudioWaveInfo;
|
||||
|
||||
typedef struct AudioResService {
|
||||
ServiceHeader header;
|
||||
|
||||
ModResult (*replace_wave)(
|
||||
ModContext* ctx,
|
||||
AudioWaveBank bank,
|
||||
uint16_t wave_id,
|
||||
char const* file_name,
|
||||
AudioWaveInfo const* wave_info,
|
||||
AudioWaveHandle* out_handle);
|
||||
} AudioResService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<AudioResService> {
|
||||
static constexpr const char* id = AUDIO_RES_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = AUDIO_RES_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = AUDIO_RES_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,227 @@
|
||||
#include "audio_res.hpp"
|
||||
|
||||
#include "helpers/cast.hpp"
|
||||
#include "mods/svc/audio_res.h"
|
||||
|
||||
#include "../registry.hpp"
|
||||
#include "../slot_map.hpp"
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
namespace audio_res {
|
||||
namespace {
|
||||
|
||||
using namespace dusk::helpers::cast;
|
||||
|
||||
aurora::Module Log("dusk::mods::svc::audio_res");
|
||||
|
||||
SlotMap<RuntimeWaveReplacementSlot> s_waveReplacements;
|
||||
|
||||
constexpr ContainerLoadFunction s_containerLoadFunctions[] = {
|
||||
load_wav,
|
||||
#if DUSK_OPUS
|
||||
load_opus,
|
||||
#endif
|
||||
};
|
||||
|
||||
bool validate_raw_size(LoadedMod const& mod, std::string const& path, uintptr_t actual_size, AudioRawWave const& raw, u32& sample_count) {
|
||||
u32 samples_per_block;
|
||||
u32 bytes_per_block;
|
||||
switch (raw.format) {
|
||||
case Adpcm4:
|
||||
samples_per_block = 16;
|
||||
bytes_per_block = 9;
|
||||
break;
|
||||
case Pcm16:
|
||||
samples_per_block = 1;
|
||||
bytes_per_block = 2;
|
||||
break;
|
||||
default:
|
||||
Log.error("[{}] unimplemented wave format (ADPCM2/PCM8): '{}'", mod.metadata.id, path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actual_size % bytes_per_block != 0) {
|
||||
Log.error("[{}] raw file is not divisible by format block size: '{}'", mod.metadata.id, path);
|
||||
return false;
|
||||
}
|
||||
|
||||
sample_count = (actual_size / bytes_per_block) * samples_per_block;
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult load_raw(
|
||||
LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, AudioRawWave const& raw) {
|
||||
auto file_contents = mod.bundle->readFile(slot.bundle_path);
|
||||
u32 sample_count = 0;
|
||||
|
||||
if (!validate_raw_size(mod, slot.bundle_path, file_contents.size(), raw, sample_count)) {
|
||||
Log.error(
|
||||
"[{}] replace_wave: file '{}' is a raw .abf file, but raw_wave data was not specified",
|
||||
mod.metadata.id, slot.bundle_path);
|
||||
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (slot.format == Pcm16) {
|
||||
SampleDataPcm16 pcm16;
|
||||
pcm16.data.resize(sample_count);
|
||||
memcpy(pcm16.data.data(), file_contents.data(), file_contents.size());
|
||||
slot.data = std::make_shared<SampleDataPcm16>(std::move(pcm16));
|
||||
} else {
|
||||
slot.data = std::make_shared<SampleDataU8>(std::move(file_contents));
|
||||
}
|
||||
|
||||
slot.sample_count = sample_count;
|
||||
slot.sample_rate = raw.sample_rate;
|
||||
slot.format = raw.format;
|
||||
slot.sample_value_penult = raw.sample_value_penult;
|
||||
slot.sample_value_last = raw.sample_value_last;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an audio file stored in some sort of container format.
|
||||
* File type is determined by looking at contents, not extension.
|
||||
*/
|
||||
ModResult load_container(LoadedMod const& mod, RuntimeWaveReplacementSlot& slot) {
|
||||
auto file_contents = mod.bundle->readFile(slot.bundle_path);
|
||||
|
||||
for (auto const loader : s_containerLoadFunctions) {
|
||||
auto const result = loader(mod, slot, file_contents);
|
||||
if (result == MOD_OK) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
if (result != MOD_UNSUPPORTED) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
|
||||
JASWaveInfo wave_info_from_slot(RuntimeWaveReplacementSlot const& slot) {
|
||||
JASWaveInfo info;
|
||||
info.mWaveFormat = static_cast<u8>(slot.format);
|
||||
info.mBaseKey = slot.base_key;
|
||||
info.mLoopFlag = slot.loop ? 0xFF : 0;
|
||||
info.mSampleRate = slot.sample_rate;
|
||||
info.mOffsetStart = 0; // Unused but just init it ig.
|
||||
info.mOffsetLength = bounded_cast(slot.data->size());
|
||||
info.mLoopStartSample = slot.loop_start_sample;
|
||||
info.mLoopEndSample = bounded_cast(slot.loop_end_sample);
|
||||
info.mSampleCount = bounded_cast(slot.sample_count);
|
||||
info.mpLast = slot.sample_value_last;
|
||||
info.mpPenult = slot.sample_value_penult;
|
||||
// mpLoaded is initialized to point to a 1, so we're good there.
|
||||
return info;
|
||||
}
|
||||
|
||||
ModResult insert_replace_wave(
|
||||
ModContext* ctx,
|
||||
AudioWaveBank bank,
|
||||
u16 wave_id,
|
||||
char const* file_name,
|
||||
AudioWaveInfo const* wave_info,
|
||||
AudioWaveHandle* out_handle) {
|
||||
if (out_handle != nullptr) {
|
||||
*out_handle = 0;
|
||||
}
|
||||
|
||||
auto mod = mod_from_context(ctx);
|
||||
if (mod == nullptr || file_name == nullptr || !is_safe_resource_path(file_name)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
RuntimeWaveReplacementSlot slot{};
|
||||
slot.bundle_path = file_name;
|
||||
slot.bank = bank;
|
||||
slot.wave_id = wave_id;
|
||||
|
||||
ModResult result;
|
||||
if (wave_info && wave_info->raw_wave) {
|
||||
result = load_raw(*mod, slot, *wave_info->raw_wave);
|
||||
} else {
|
||||
result = load_container(*mod, slot);
|
||||
}
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (wave_info != nullptr) {
|
||||
slot.base_key = wave_info->base_key;
|
||||
slot.loop = wave_info->loop;
|
||||
slot.loop_start_sample = wave_info->loop_start_sample;
|
||||
slot.loop_end_sample = wave_info->loop_end_sample;
|
||||
} else {
|
||||
slot.base_key = AUDIO_RES_DEFAULT_KEY;
|
||||
slot.loop = false;
|
||||
slot.loop_start_sample = 0;
|
||||
slot.loop_end_sample = slot.sample_count;
|
||||
}
|
||||
|
||||
// TODO: Handle conflicts
|
||||
|
||||
audio_res::AudioWaveReplacementValue value;
|
||||
value.data = slot.data;
|
||||
value.wave_info = wave_info_from_slot(slot);
|
||||
audio_res::s_replacements.emplace(
|
||||
audio_res::AudioWaveKey{.bank = bank, .wave_id = wave_id},
|
||||
std::make_unique<AudioWaveReplacementValue>(std::move(value)));
|
||||
|
||||
const auto handle = s_waveReplacements.emplace(*mod, std::move(slot));
|
||||
if (out_handle) {
|
||||
*out_handle = handle;
|
||||
}
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
constexpr AudioResService s_audioResService{
|
||||
.header = SERVICE_HEADER(AudioResService, AUDIO_RES_SERVICE_MAJOR, AUDIO_RES_SERVICE_MINOR),
|
||||
.replace_wave = &audio_res::insert_replace_wave};
|
||||
|
||||
}
|
||||
|
||||
absl::flat_hash_map<AudioWaveKey, std::unique_ptr<AudioWaveReplacementValue>> s_replacements;
|
||||
|
||||
AudioWaveReplacementValue::~AudioWaveReplacementValue() = default;
|
||||
const JASWaveInfo* AudioWaveReplacementValue::getWaveInfo() const {
|
||||
return &wave_info;
|
||||
}
|
||||
|
||||
void const* AudioWaveReplacementValue::getAramBaseAddress() const {
|
||||
return data->get_data().data();
|
||||
}
|
||||
|
||||
intptr_t AudioWaveReplacementValue::getWavePtr() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SampleDataPcm16::be_swap() {
|
||||
for (auto& sample : data) {
|
||||
::be_swap(sample);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace audio_res
|
||||
|
||||
constinit const ServiceModule g_audioResModule{
|
||||
.id = AUDIO_RES_SERVICE_ID,
|
||||
.majorVersion = AUDIO_RES_SERVICE_MAJOR,
|
||||
.minorVersion = AUDIO_RES_SERVICE_MINOR,
|
||||
.service = &audio_res::s_audioResService,
|
||||
// .modDetached = overlay_remove_mod,
|
||||
// .lifecycleApplied = overlay_sync_files,
|
||||
// .frameEnd =
|
||||
// [] {
|
||||
// if (consume_overlays_dirty()) {
|
||||
// overlay_sync_files();
|
||||
// }
|
||||
// },
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include "JSystem/JAudio2/JASWaveInfo.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "mods/svc/audio_res.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
}
|
||||
namespace dusk::mods::svc::audio_res {
|
||||
|
||||
struct SampleDataBuf {
|
||||
virtual ~SampleDataBuf() = default;
|
||||
[[nodiscard]] virtual std::span<std::byte const> get_data() const = 0;
|
||||
[[nodiscard]] size_t size() const {
|
||||
return get_data().size();
|
||||
}
|
||||
};
|
||||
|
||||
// Making a separate type for this rather than just using a vector<u8>
|
||||
// because I don't want aliasing or alignment to eat my face.
|
||||
struct SampleDataPcm16 : SampleDataBuf {
|
||||
std::vector<s16> data;
|
||||
|
||||
[[nodiscard]] std::span<std::byte const> get_data() const override {
|
||||
return as_bytes(std::span(data));
|
||||
}
|
||||
|
||||
void be_swap();
|
||||
};
|
||||
|
||||
struct SampleDataU8 : SampleDataBuf {
|
||||
std::vector<u8> data;
|
||||
|
||||
explicit SampleDataU8(std::vector<u8> data) : data(std::move(data)) {}
|
||||
|
||||
[[nodiscard]] std::span<std::byte const> get_data() const override {
|
||||
return as_bytes(std::span(data));
|
||||
}
|
||||
};
|
||||
|
||||
struct AudioWaveKey {
|
||||
AudioWaveBank bank;
|
||||
u32 wave_id;
|
||||
|
||||
template <typename H>
|
||||
friend H AbslHashValue(H h, const AudioWaveKey& k) {
|
||||
return H::combine(std::move(h), k.bank, k.wave_id);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool operator==(const AudioWaveKey& other) const {
|
||||
return other.bank == bank && other.wave_id == wave_id;
|
||||
}
|
||||
};
|
||||
|
||||
struct AudioWaveReplacementValue : JASWaveHandle {
|
||||
~AudioWaveReplacementValue() override;
|
||||
[[nodiscard]] const JASWaveInfo* getWaveInfo() const override;
|
||||
[[nodiscard]] intptr_t getWavePtr() const override;
|
||||
[[nodiscard]] void const* getAramBaseAddress() const override;
|
||||
|
||||
JASWaveInfo wave_info;
|
||||
std::shared_ptr<SampleDataBuf> data;
|
||||
};
|
||||
|
||||
extern absl::flat_hash_map<AudioWaveKey, std::unique_ptr<AudioWaveReplacementValue>> s_replacements;
|
||||
|
||||
struct RuntimeWaveReplacementSlot {
|
||||
std::string bundle_path;
|
||||
AudioWaveBank bank;
|
||||
u16 wave_id;
|
||||
|
||||
u8 base_key;
|
||||
bool loop;
|
||||
u32 loop_start_sample;
|
||||
u32 loop_end_sample;
|
||||
|
||||
AudioWaveFormat format;
|
||||
f32 sample_rate;
|
||||
u32 sample_count;
|
||||
s16 sample_value_last;
|
||||
s16 sample_value_penult;
|
||||
|
||||
std::shared_ptr<audio_res::SampleDataBuf> data;
|
||||
};
|
||||
|
||||
using ContainerLoadFunction = ModResult (*)(LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span<u8 const> fileData);
|
||||
|
||||
ModResult load_wav(LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span<u8 const> fileData);
|
||||
ModResult load_opus(LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span<u8 const> fileData);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#if DUSK_OPUS
|
||||
|
||||
#include "audio_res.hpp"
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "helpers/cast.hpp"
|
||||
#include "opusfile.h"
|
||||
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::svc::audio_res::opus");
|
||||
|
||||
struct OpusHandle {
|
||||
OggOpusFile* file;
|
||||
|
||||
~OpusHandle() {
|
||||
op_free(file);
|
||||
}
|
||||
|
||||
operator OggOpusFile*() const {
|
||||
return file;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace dusk::mods::svc::audio_res {
|
||||
ModResult load_opus(LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span<u8 const> fileData) {
|
||||
OpusHead head;
|
||||
const int result = op_test(&head, fileData.data(), fileData.size());
|
||||
if (result != 0) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
|
||||
if (head.channel_count != 1) {
|
||||
Log.error("[{}] replace_wave: file '{}' must be mono but actually has {} channels", mod.metadata.id, slot.bundle_path, head.channel_count);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (head.stream_count != 1) {
|
||||
Log.error("[{}] replace_wave: file '{}' has multiple Opus streams!", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
int error;
|
||||
auto const handle_raw = op_open_memory(fileData.data(), fileData.size(), &error);
|
||||
if (error != 0) {
|
||||
Log.error("[{}] replace_wave: file '{}': unable to open Opus file: {}", mod.metadata.id, slot.bundle_path, error);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
const OpusHandle handle(handle_raw);
|
||||
auto const length = op_pcm_total(handle, -1);
|
||||
if (length < 0) {
|
||||
Log.error("[{}] replace_wave: file '{}': failed to check stream length?", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
SampleDataPcm16 pcm_buffer;
|
||||
pcm_buffer.data.resize(length);
|
||||
|
||||
std::span span_pcm(pcm_buffer.data);
|
||||
|
||||
while (!span_pcm.empty()) {
|
||||
auto read_result = op_read(
|
||||
handle,
|
||||
span_pcm.data(), helpers::cast::bounded_cast(span_pcm.size()),
|
||||
nullptr);
|
||||
|
||||
if (read_result <= 0) {
|
||||
Log.error("[{}] replace_wave: file '{}': failed to read Opus: {}", mod.metadata.id, slot.bundle_path, read_result);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
if (read_result > span_pcm.size()) {
|
||||
Log.fatal("What teh fuck?");
|
||||
}
|
||||
|
||||
span_pcm = span_pcm.subspan(read_result);
|
||||
}
|
||||
|
||||
pcm_buffer.be_swap();
|
||||
|
||||
slot.data = std::make_shared<SampleDataPcm16>(std::move(pcm_buffer));
|
||||
slot.sample_rate = 48000; // Opus always decodes at 48 kHz.
|
||||
slot.sample_count = length;
|
||||
slot.format = Pcm16;
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "audio_res.hpp"
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "helpers/alignment.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::svc::audio_res::wav");
|
||||
|
||||
struct ChunkHeader {
|
||||
char magic[4];
|
||||
LE(u32) size;
|
||||
};
|
||||
|
||||
struct RiffChunk {
|
||||
ChunkHeader header; // magic "RIFF"
|
||||
char id[4]; // "WAVE"
|
||||
// Data after chunk.
|
||||
};
|
||||
|
||||
constexpr u16 WAVE_FORMAT_PCM = 0x0001;
|
||||
|
||||
struct FmtPcmChunkData {
|
||||
ChunkHeader header;
|
||||
|
||||
u16 wFormatTag;
|
||||
u16 nChannels;
|
||||
u32 nSamplesPerSec;
|
||||
u32 nAvgBytesPerSec;
|
||||
u16 nBlockAlign;
|
||||
u16 wBitsPerSample;
|
||||
};
|
||||
|
||||
bool check_four_cc(char const (&field)[4], char const (&expected)[5]) {
|
||||
return memcmp(field, expected, 4) == 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace dusk::mods::svc::audio_res {
|
||||
|
||||
ModResult load_wav(LoadedMod const& mod, RuntimeWaveReplacementSlot& slot, std::span<u8 const> fileData) {
|
||||
using namespace helpers::alignment;
|
||||
|
||||
if (fileData.size() < sizeof(RiffChunk)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
|
||||
auto const riffChunk = read_unaligned<RiffChunk>(&fileData[0]);
|
||||
if (!check_four_cc(riffChunk.header.magic, "RIFF")) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
|
||||
if (!check_four_cc(riffChunk.id, "WAVE")) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
|
||||
if (sizeof(ChunkHeader) + riffChunk.header.size > fileData.size()) {
|
||||
Log.error("[{}] wav file {}: invalid size!", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
bool has_read_fmt = false;
|
||||
SampleDataPcm16 pcmData;
|
||||
|
||||
auto waveData = fileData.subspan(sizeof(RiffChunk), riffChunk.header.size);
|
||||
while (waveData.size() > sizeof(ChunkHeader)) {
|
||||
auto const chunkHeader = read_unaligned<ChunkHeader>(&waveData[0]);
|
||||
|
||||
if (check_four_cc(chunkHeader.magic, "fmt ")) {
|
||||
if (has_read_fmt) {
|
||||
Log.error("[{}] wav file {}: multiple fmt chunks!", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (waveData.size() < sizeof(FmtPcmChunkData)) {
|
||||
Log.error("[{}] wav file {}: fmt chunk too small!", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto const fmtChunk = read_unaligned<FmtPcmChunkData>(&waveData[0]);
|
||||
|
||||
if (fmtChunk.nChannels != 1) {
|
||||
Log.error("[{}] wav file {}: only mono audio is supported", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (fmtChunk.wFormatTag != WAVE_FORMAT_PCM) {
|
||||
Log.error("[{}] wav file {}: only 16-bit PCM is supported", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (fmtChunk.wBitsPerSample != 16) {
|
||||
Log.error("[{}] wav file {}: only 16-bit PCM is supported", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
has_read_fmt = true;
|
||||
slot.sample_rate = static_cast<f32>(fmtChunk.nSamplesPerSec);
|
||||
|
||||
} else if (check_four_cc(chunkHeader.magic, "data")) {
|
||||
if (sizeof(chunkHeader) + chunkHeader.size > fileData.size()) {
|
||||
Log.error("[{}] wav file {}: unexpected EOF reading sample data", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto const sampleData = waveData.subspan(sizeof(chunkHeader), chunkHeader.size);
|
||||
for (size_t i = 0; i + 1 < sampleData.size(); i += 2) {
|
||||
pcmData.data.push_back(read_unaligned<LE(s16)>(&sampleData[i]));
|
||||
}
|
||||
}
|
||||
|
||||
auto skip = sizeof(chunkHeader) + chunkHeader.size;
|
||||
if (skip % 2 != 0) {
|
||||
skip += 1;
|
||||
}
|
||||
|
||||
if (waveData.size() < skip) {
|
||||
Log.error("[{}] wav file {}: unexpected EOF", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
waveData = waveData.subspan(skip);
|
||||
}
|
||||
|
||||
if (!has_read_fmt) {
|
||||
Log.error("[{}] wav file {}: missing fmt chunk", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (pcmData.data.empty()) {
|
||||
Log.error("[{}] wav file {}: missing data chunk", mod.metadata.id, slot.bundle_path);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
pcmData.be_swap();
|
||||
|
||||
slot.format = Pcm16;
|
||||
slot.sample_count = pcmData.size() / sizeof(u16);
|
||||
slot.data = std::make_unique<SampleDataPcm16>(std::move(pcmData));
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -210,6 +210,7 @@ void ModLoader::init_services() {
|
||||
&svc::g_gameModule,
|
||||
&svc::g_cameraModule,
|
||||
&svc::g_gfxModule,
|
||||
&svc::g_audioResModule,
|
||||
})
|
||||
{
|
||||
svc::register_module(*module);
|
||||
|
||||
@@ -72,5 +72,6 @@ extern const ServiceModule g_uiModule;
|
||||
extern const ServiceModule g_gameModule;
|
||||
extern const ServiceModule g_cameraModule;
|
||||
extern const ServiceModule g_gfxModule;
|
||||
extern const ServiceModule g_audioResModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
Reference in New Issue
Block a user