Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
30 changes: 18 additions & 12 deletions core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) {
context.summaryPeak = 0.0f;
}

static Clock::time_point WaitForRoom(AudioOut2Context& context, std::unique_lock<std::mutex>& lock) {
auto now = Clock::now();
const auto waitStart = now;
while (QueueLevel(context, now) >= context.queueDepth && now - waitStart <= FULL_WAIT_TIMEOUT) {
lock.unlock();
PreciseSleepUs(std::chrono::microseconds(FULL_WAIT_STEP).count());
lock.lock();
now = Clock::now();
}
return now;
}

extern "C" {

int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try {
Expand Down Expand Up @@ -368,19 +380,12 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block
std::unique_lock lock(context->lock);
const QueueLevelPublisher publication{*context, lock};
auto now = Clock::now();
const auto waitStart = now;
while (QueueLevel(*context, now) >= context->queueDepth) {
if (!blocking) {
context->fullRejects++;
if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast<void*>(context), context->queueDepth);
return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL;
}
if (now - waitStart > FULL_WAIT_TIMEOUT) break;
lock.unlock();
PreciseSleepUs(std::chrono::microseconds(FULL_WAIT_STEP).count());
lock.lock();
now = Clock::now();
if (!blocking && QueueLevel(*context, now) >= context->queueDepth) {
context->fullRejects++;
if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast<void*>(context), context->queueDepth);
return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL;
}
if (blocking) now = WaitForRoom(*context, lock);
if (context->advancedMode && context->advancedGrains.empty()) {
if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented");
if (!context->grainReady.wait(lock, LibcShutdownToken_nid_postfix(), [&] { return !context->advancedGrains.empty(); })) throw ProcessShutdown{};
Expand Down Expand Up @@ -421,6 +426,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block
context->output.insert(context->output.end(), context->mix.begin(), context->mix.end());
}
}
if (blocking) now = WaitForRoom(*context, lock);
context->pushes++;
context->summaryPushes++;
if (blocking) context->blockingPushes++;
Expand Down
25 changes: 25 additions & 0 deletions core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,30 @@ void TestLevelWithoutADeviceUsesThePlaybackClock() {
Require(level == 0 && available == queueDepth);
}

void TestBlockingPushWaitsForRoomWithoutADevice() {
SetEnvironment("SDL_DISKAUDIOFILE", std::filesystem::temp_directory_path().string());
constexpr std::uint32_t paced = 1024;
AudioOut2ContextParam params{};
Require(sceAudioOut2ContextResetParam(&params) == 0);
params.num_grains = paced;
params.queue_depth = queueDepth;
AudioOut2ContextHandle handle = 0;
Require(sceAudioOut2ContextCreate(&params, nullptr, 0, &handle) == 0);
Require(reinterpret_cast<AudioOut2Context*>(handle)->device == 0);
const auto grainTime = std::chrono::duration<double>(static_cast<double>(paced) / frequency);
std::uint32_t level = 99;
std::uint32_t available = 99;
for (std::uint32_t round = 0; round < 4; round++) {
const auto start = std::chrono::steady_clock::now();
Require(sceAudioOut2ContextPush(handle, 1) == 0);
const auto took = std::chrono::steady_clock::now() - start;
Require(sceAudioOut2ContextGetQueueLevel(handle, &level, &available) == 0);
Require(level < queueDepth && available > 0);
if (round + 1 >= queueDepth) Require(took >= grainTime / 2);
}
Require(sceAudioOut2ContextDestroy(handle) == 0);
}

void TestPrimingAfterRunningDry() {
Session session("anyps5_audio_out2_priming.raw");
session.Fill(0.5f);
Expand Down Expand Up @@ -359,6 +383,7 @@ int main() {
TestDeviceLevelDoesNotWaitForTheContext(false);
TestDeviceLevelDoesNotWaitForTheContext(true);
TestLevelWithoutADeviceUsesThePlaybackClock();
TestBlockingPushWaitsForRoomWithoutADevice();
TestPrimingAfterRunningDry();
TestLowLatencyCallsRejectBadArguments();
TestLowLatencyReadsAtNextPush();
Expand Down
1 change: 1 addition & 0 deletions docs/dev/debt/libSceAudioOut.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@

### Functional

- [sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - a blocking Push waits for room before it queues its grain and again after, so it returns once fewer grains than the queue depth wait to be played (each wait ends after at most 200 ms). Inferred from PPSA02530, whose sound engine pushes one grain per call from a feeder thread and wakes its renderer after each push: while Push returned as soon as the grain was queued, the renderer found the queue full and slept until the next rendered frame. When the console's blocking Push returns is unmeasured.
- [sceAudioOutMasteringInit, sceAudioOutMasteringTerm and sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - non-zero flags, a null pointer and a params id other than 1 throw; a repeated Init, a Term without Init and a SetParam or GetState before Init are unknown. There is no mastering stage, so Init and Term keep no state and SetParam reads only the id. The signatures and the default params id 1 follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut.cs#L312-L344), not a console measurement. SharpProspero documents flag bit 0 as `SwapEqCompressor`, and shadPS4's PS4 stub [rejects any non-zero Init flags](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/audio/audioout.cpp#L956-L962) with 0x80260201
- [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the states id 1 and the meter offsets 32, 128, 240, 272 and 304 in a 336-byte struct follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Audio/AudioOutMastering.cs#L97-L110), not a console measurement. With no mastering stage it zeroes the compressor input RMS and gain, and the limiter input, output and gain peaks, and leaves the other bytes, including the descriptors the AudioOut2 call writes, untouched; zero is not known to be what the console reports for an idle chain. A null pointer and a states id other than 1 throw; the console error codes are unknown
- [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without a chat service; repeated-call and no-session behavior therefore still depends on the missing service.
Expand Down
Loading