Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,28 @@ if(BUILD_TESTING)
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_seamless_asset_probe.py")
endif()
endif()

if(BUILD_TESTING)
add_executable(tomba-audio-sync-tests tests/test_audio_sync_mod.c
src/mods/tomba_audio_sync_plugin.c "${PSXRECOMP_ROOT}/runtime/src/audio_sync.c")
target_include_directories(tomba-audio-sync-tests PRIVATE "${PSXRECOMP_ROOT}/runtime/include")
add_test(NAME tomba_audio_sync COMMAND tomba-audio-sync-tests)
add_executable(tomba-audio-sync-persistence-tests
tests/test_audio_sync_persistence.cpp
"${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp"
"${PSXRECOMP_ROOT}/runtime/src/mod_media.cpp"
"${PSXRECOMP_ROOT}/runtime/src/iso_reader.cpp"
"${PSXRECOMP_ROOT}/runtime/src/cue_sheet.cpp"
"${PSXRECOMP_ROOT}/runtime/src/crc32.c"
"${PSXRECOMP_ROOT}/runtime/src/psx_sha256.c")
target_include_directories(tomba-audio-sync-persistence-tests PRIVATE
"${PSXRECOMP_ROOT}/runtime/include" "${PSXRECOMP_ROOT}/recompiler/include"
"${PSXRECOMP_ROOT}/recompiler/lib/toml11")
target_link_libraries(tomba-audio-sync-persistence-tests PRIVATE chdr-static)
add_test(NAME tomba_audio_sync_persistence COMMAND tomba-audio-sync-persistence-tests
"${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded/packages/psx.audio.low-latency/1.0.1/manifest.toml")
add_test(NAME tomba_audio_sync_catalog COMMAND "${CMAKE_COMMAND}"
"-DTOMBA_ROOT=${CMAKE_CURRENT_SOURCE_DIR}"
"-DSCRATCH=${CMAKE_CURRENT_BINARY_DIR}/audio-catalog-tests"
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_audio_sync_catalog.cmake")
endif()
17 changes: 17 additions & 0 deletions cmake/TombaAudioSync.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Merge one shared audio declaration into the existing regional mod staging.
function(tomba_audio_sync_catalog output original)
set(staged "${CMAKE_CURRENT_BINARY_DIR}/audio-sync-preloaded")
# Reconfiguration must not keep packages removed by a regional revision.
# Only our fixed generated staging directory may be replaced.
cmake_path(IS_PREFIX CMAKE_CURRENT_BINARY_DIR "${staged}" NORMALIZE within_build)
if(NOT within_build OR "${staged}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}")
message(FATAL_ERROR "Audio catalog staging must stay inside the build directory")
endif()
file(REMOVE_RECURSE "${staged}")
if(NOT "${original}" STREQUAL "NONE")
file(COPY "${original}/" DESTINATION "${staged}")
endif()
file(COPY "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../mods/preloaded/packages/psx.audio.low-latency"
DESTINATION "${staged}/packages")
set(${output} "${staged}" PARENT_SCOPE)
endfunction()
29 changes: 29 additions & 0 deletions docs/regional/audio-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Audio Sync Fix

Audio Sync Fix is enabled by default for new profiles. It reduces host buffering
and prevents accelerated boot/fast-forward audio from building a delayed queue.
After a real presentation underrun it also avoids queuing accelerated catch-up.
Guest speed and SPU simulation are unchanged. Fast-forward is intentionally mute.

ON uses a 70 ms target by default; OFF restores the game's configured fallback.
The Audio Buffer slider offers 20–180 ms in 5 ms steps. 70 ms is Recommended;
lower values are Experimental (20 ms is known unstable), and higher values give
more reserve. Both views use the same saved mod option. Reset to Defaults resets
only that target to 70 ms. Existing enabled/disabled and target choices remain
authoritative. Changes apply at the next game start, not to a running device.
High Quality SPU / Enhanced Audio is independent. Quick Mods and Mods use the
same normal saved feature state. No account or network service is involved.

Audio Diagnostics optionally emits target, actual fill and host lifecycle
counters once per second to the runtime log. It is off by default. Fill is not
measured end-to-end output latency; Windows, HDMI and receivers add delay that
has not been measured here. Underruns count depletion episodes (including some
intentional draining), overflow counts dropped frames, and device closes do not
include driver-internal resets. There is no callback file I/O.

A long display-mode switch may still cause brief silence while recovering.
These host-buffer changes do not measure or control external TV/receiver delay.
The shared runtime uses no regional game addresses. Platform and regional
listening checks remain necessary; this is not a campaign-wide guarantee.
The owner confirmed 70 ms without audible issues on one system. Lower targets
can stutter or drop out; no universal safe minimum is claimed.
4 changes: 4 additions & 0 deletions eu/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ if(TOMBI_PLAYER_RELEASE)
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/release/mods/packages/"
DESTINATION "${tombi_mods}/packages")
endif()
include("${TOMBA_ROOT}/cmake/TombaAudioSync.cmake")
tomba_audio_sync_catalog(tombi_mods "${tombi_mods}")
psxrecomp_add_game_runtime(${tombi_target}
WINDOW_TITLE "Tombi! Europe Rev 1"
DEFAULT_GAME_CONFIG_PATH "${tombi_config}"
Expand Down Expand Up @@ -169,3 +171,5 @@ else()
set_target_properties(${tombi_target} PROPERTIES OUTPUT_NAME "Tombi_EU_Prototype")
endif()
# Original EU profiles supply guest addresses; host policies remain shared.

target_sources(${tombi_target} PRIVATE "${TOMBA_ROOT}/src/mods/tomba_audio_sync_plugin.c")
4 changes: 4 additions & 0 deletions eu2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ if(TOMBI_EU2_SEAMLESS)
execute_process(COMMAND "${Python3_EXECUTABLE}" -I -B
"${CMAKE_CURRENT_SOURCE_DIR}/widescreen_profile.py" COMMAND_ERROR_IS_FATAL ANY)
endif()
include("${TOMBA_ROOT}/cmake/TombaAudioSync.cmake")
tomba_audio_sync_catalog(eu2_mods "${eu2_mods}")
psxrecomp_add_game_runtime(tombi-eu2
WINDOW_TITLE "Tombi! (Europe, SCES-01331)"
DEFAULT_GAME_CONFIG_PATH "game.toml"
Expand Down Expand Up @@ -112,3 +114,5 @@ if(TOMBI_EU2_SEAMLESS)
endif()
endif()
endif()

target_sources(tombi-eu2 PRIVATE "${TOMBA_ROOT}/src/mods/tomba_audio_sync_plugin.c")
4 changes: 4 additions & 0 deletions jp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ if(TOMBA_JP_ENHANCEMENTS)
execute_process(COMMAND "${Python3_EXECUTABLE}" -I -B "${CMAKE_CURRENT_SOURCE_DIR}/widescreen_profile.py" --output "${jp_generated}" COMMAND_ERROR_IS_FATAL ANY)
set(jp_mods "${CMAKE_CURRENT_SOURCE_DIR}/mods")
endif()
include("${TOMBA_ROOT}/cmake/TombaAudioSync.cmake")
tomba_audio_sync_catalog(jp_mods "${jp_mods}")
psxrecomp_add_game_runtime(tomba-jp
PGXP
WINDOW_TITLE "Ore! Tomba"
Expand Down Expand Up @@ -57,3 +59,5 @@ endif()
# The shared launcher already embeds the UTF-8 application manifest
# (recomp-ui windows/recomp_utf8.manifest); a second manifest resource would
# collide with it at link time.

target_sources(tomba-jp PRIVATE "${TOMBA_ROOT}/src/mods/tomba_audio_sync_plugin.c")
40 changes: 40 additions & 0 deletions mods/preloaded/packages/psx.audio.low-latency/1.0.1/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
format_version = 5
id = "psx.audio.low-latency"
version = "1.0.1"
name = "Audio Sync Fix"
author = "TombiRecomp contributors"
description = "Reduces audio latency and prevents excess buffering during startup, fast-forward and display mode changes."
resolver = "declarative"
save_compatibility = "shared"
[[target]]
game_id = "*"
[[feature]]
id = "low-latency-audio"
name = "Audio Sync Fix"
description = "Changes apply at the next game start. The Audio Buffer target applies at the next game start (70 ms default and recommended). Values below 70 ms are experimental; higher values provide more buffer reserve. 20 ms is known unstable. Restores low buffering after display-mode changes. Accelerated audio is discarded during fast-forward while the SPU keeps running; normal playback resumes automatically. OFF restores the original buffer. High Quality SPU and game speed are unchanged."
group = "Audio"
default_enabled = true
[[plugin]]
feature = "low-latency-audio"
id = "psx.low-latency-audio"

[[option]]
feature = "low-latency-audio"
id = "target_ms"
label = "Audio Buffer"
description = "Target buffer, not measured output latency. 70 ms default and Recommended; below 70 ms Experimental; above 70 ms more buffer reserve; 20 ms unstable. Applies at the next game start."
group = "Audio"
type = "integer"
min = 20
max = 180
step = 5
default = 70

[[option]]
feature = "low-latency-audio"
id = "diagnostics"
label = "Audio Diagnostics"
description = "Optional once-per-second target, fill, underrun, overflow and device-lifecycle diagnostics in the runtime log. Applies at next game start. No account data."
group = "Audio"
type = "boolean"
default = "false"
24 changes: 24 additions & 0 deletions src/mods/tomba_audio_sync_plugin.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* Host output policy only; shared by the official regional players. */
#include "audio_sync.h"
#include "mod_plugins.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
static void activate_audio_sync(void) {
char value[32], *end;
psx_audio_sync_set_enabled(1);
/* target_ms is an absolute buffer size, never a reduction. Only the
* committed plan is read; editing options cannot retarget a running device. */
if (psx_mod_current_option_value("target_ms", value, sizeof(value))) {
errno = 0;
const long target = strtol(value, &end, 10);
if (!errno && end != value && !*end && target >= PSX_AUDIO_SYNC_MIN_MS &&
target <= PSX_AUDIO_SYNC_MAX_MS)
(void)psx_audio_sync_set_target((int)target);
}
if (psx_mod_current_option_value("diagnostics", value, sizeof(value)))
psx_audio_sync_set_diagnostics(!strcmp(value,"true") || !strcmp(value,"1"));
}
PSX_MOD_CONSTRUCTOR(tomba_register_audio_sync_plugin) {
(void)psx_mod_register_activation_plugin("psx.low-latency-audio", activate_audio_sync);
}
20 changes: 20 additions & 0 deletions tests/test_audio_sync_catalog.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.20)
if(NOT IS_DIRECTORY "${TOMBA_ROOT}/cmake" OR NOT SCRATCH)
message(FATAL_ERROR "Pass TOMBA_ROOT and a private build SCRATCH directory")
endif()
string(RANDOM LENGTH 12 ALPHABET 0123456789abcdef nonce)
set(CMAKE_CURRENT_BINARY_DIR "${SCRATCH}/run-${nonce}")
set(original "${CMAKE_CURRENT_BINARY_DIR}/original")
file(MAKE_DIRECTORY "${original}/packages/revision-only/1.0.0")
file(WRITE "${original}/packages/revision-only/1.0.0/manifest.toml" "test fixture")
include("${TOMBA_ROOT}/cmake/TombaAudioSync.cmake")
tomba_audio_sync_catalog(staged "${original}")
if(NOT EXISTS "${staged}/packages/revision-only/1.0.0/manifest.toml")
message(FATAL_ERROR "Regional declarations were not preserved")
endif()
file(RENAME "${original}/packages/revision-only" "${CMAKE_CURRENT_BINARY_DIR}/removed")
tomba_audio_sync_catalog(staged "${original}")
if(EXISTS "${staged}/packages/revision-only" OR NOT EXISTS "${staged}/packages/psx.audio.low-latency/1.0.1/manifest.toml")
message(FATAL_ERROR "Stale regional package or missing shared audio declaration")
endif()
message(STATUS "PASS shared audio staging preserves current regional declarations without stale revision packages")
31 changes: 31 additions & 0 deletions tests/test_audio_sync_mod.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#ifdef NDEBUG
#undef NDEBUG
#endif
#include "audio_sync.h"
#include "mod_plugins.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static PSXModActivationCallback activate;
static const char *option;
int psx_mod_register_activation_plugin(const char *id,PSXModActivationCallback fn) {
assert(!strcmp(id,"psx.low-latency-audio")); activate=fn; return 1;
}
int psx_mod_current_option_value(const char *id,char *out,uint32_t size) {
if (strcmp(id,"target_ms") || !option || strlen(option)>=size) return 0;
strcpy(out,option); return 1;
}
int main(void) {
assert(activate);
psx_audio_sync_set_enabled(0); assert(psx_audio_sync_target(160)==160);
option=NULL; activate(); assert(psx_audio_sync_target(180)==70);
option="50"; activate(); assert(psx_audio_sync_target(180)==50);
/* Editing the provider alone does not alter the running session. */
option="80"; assert(psx_audio_sync_target(180)==50);
psx_audio_sync_set_enabled(0); activate(); assert(psx_audio_sync_target(180)==80);
option="180"; psx_audio_sync_set_enabled(0); activate(); assert(psx_audio_sync_target(180)==180);
const char *bad[]={"19","181","71","70oops","999999999999999999999999999"};
for(int i=0;i<5;i++) { option=bad[i]; psx_audio_sync_set_enabled(0); activate(); assert(psx_audio_sync_target(180)==70); }
psx_audio_sync_set_enabled(0); assert(psx_audio_sync_target(180)==180);
puts("PASS default70, saved target, next-start activation, validation and OFF fallback");
}
40 changes: 40 additions & 0 deletions tests/test_audio_sync_persistence.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "mod_packages.h"
#include <chrono>
#include <filesystem>
#include <iostream>
#include <stdexcept>

using namespace PSXRecompV4;
namespace fs = std::filesystem;
static void check(bool value, const std::string& message) {
if (!value) throw std::runtime_error(message);
}
int main(int argc, char** argv) {
check(argc == 2, "Pass the Audio Sync manifest path");
const auto nonce = std::chrono::steady_clock::now().time_since_epoch().count();
const fs::path root = fs::temp_directory_path() /
("tomba-audio-persistence-" + std::to_string(nonce));
const fs::path package = root / "packages/psx.audio.low-latency/1.0.1";
fs::create_directories(package);
fs::copy_file(argv[1], package / "manifest.toml");
const char* id = "psx.audio.low-latency";
const char* feature = "low-latency-audio";
const char* option = "target_ms";
std::string error;
ModPackageManager fresh(root);
check(fresh.scan(&error) && fresh.load_state(&error), error);
check(fresh.feature_option_value(id, feature, option) == "70", "fresh 70");
check(fresh.set_feature_option(id, feature, option, "80", &error) &&
fresh.save_state(&error), error);
ModPackageManager saved(root);
check(saved.scan(&error) && saved.load_state(&error), error);
check(saved.feature_option_value(id, feature, option) == "80", "saved 80 retained");
check(saved.set_feature_option(id, feature, option, "180", &error), "upper bound");
check(saved.set_feature_option(id, feature, option, "70", &error) &&
saved.save_state(&error), error);
ModPackageManager reset(root);
check(reset.scan(&error) && reset.load_state(&error), error);
check(reset.feature_option_value(id, feature, option) == "70", "reset 70 retained");
fs::remove_all(root);
std::cout << "PASS fresh70, saved80, upper180 and reset70 persistence\n";
}