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:
PJB3005
2026-07-25 16:02:20 +02:00
parent 0c0bc7c9e2
commit 98b203fd26
10 changed files with 665 additions and 0 deletions
+25
View File
@@ -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)
+4
View File
@@ -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
+11
View File
@@ -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;
+65
View File
@@ -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
+227
View File
@@ -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();
// }
// },
};
}
+93
View File
@@ -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);
}
+93
View File
@@ -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
+145
View File
@@ -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;
}
}
+1
View File
@@ -210,6 +210,7 @@ void ModLoader::init_services() {
&svc::g_gameModule,
&svc::g_cameraModule,
&svc::g_gfxModule,
&svc::g_audioResModule,
})
{
svc::register_module(*module);
+1
View File
@@ -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