diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index c58bd4c542..b09b4e444c 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -56,6 +56,12 @@ struct AudioOut2CapturedPort { using AudioOut2Grain = std::vector; +struct AudioOut2QueuedData { + const void* data = nullptr; + std::vector pcm; + bool reused = false; +}; + struct AudioOut2Port { bool used = false; std::uint64_t generation = 0; @@ -68,9 +74,11 @@ struct AudioOut2Port { bool int16 = false; const AudioOut2StereoFold* fold = nullptr; const void* data = nullptr; - std::deque queuedData; + std::deque queuedData; const void* lastBuffer = nullptr; const void* repeatedData = nullptr; + std::vector repeatedPcm; + bool repeatedReused = false; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; std::uint64_t attributeTraces = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 14c9bc2c63..bb8d062247 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -109,13 +109,20 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snaps auto& port = g_ports[index]; if (!port.used || port.context != &context || port.channels == 0) continue; const void* data = port.repeatedData; + const std::vector* setPcm = port.repeatedReused ? &port.repeatedPcm : nullptr; + AudioOut2QueuedData queued; if (!port.queuedData.empty()) { - data = port.queuedData.front(); + queued = std::move(port.queuedData.front()); port.queuedData.pop_front(); + data = queued.data; + setPcm = queued.reused ? &queued.pcm : nullptr; } if (data != nullptr) { AudioOut2CapturedPort captured{index, port.generation, data}; - if (snapshot) { + if (setPcm != nullptr) { + captured.pcm = *setPcm; + std::copy_n(port.volume, port.channels, captured.volume.begin()); + } else if (snapshot) { captured.pcm.resize(static_cast(context.grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); std::memcpy(captured.pcm.data(), data, captured.pcm.size()); std::copy_n(port.volume, port.channels, captured.volume.begin()); @@ -159,6 +166,24 @@ void AudioOut2ReleasePorts(const AudioOut2Context& context) { } } +static std::vector CopyGrain(const AudioOut2Port& port) { + if (port.data == nullptr || port.context == nullptr) return {}; + std::vector pcm(static_cast(port.context->grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); + std::memcpy(pcm.data(), port.data, pcm.size()); + return pcm; +} + +static void MarkBufferReused(const AudioOut2Port& setter) { + if (setter.data == nullptr) return; + for (auto& port : g_ports) { + if (!port.used || &port == &setter || port.context != setter.context) continue; + for (auto& queued : port.queuedData) { + if (queued.data == setter.data) queued.reused = true; + } + if (port.repeatedData == setter.data) port.repeatedReused = true; + } +} + static void TraceAttribute(AudioOut2PortHandle handle, const AudioOut2Port& port, const AudioOut2Attribute& attribute, const char* verdict) { if (!AudioOut2TraceEnabled()) return; char values[128] = ""; @@ -266,9 +291,13 @@ int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const Audi verdict = "null value"; } else if (attribute.attribute_id == ATTRIBUTE_DATA && attribute.value_size == sizeof(entry->data)) { std::memcpy(&entry->data, attribute.value, sizeof(entry->data)); - entry->queuedData.push_back(entry->data); - if (entry->queuedData.size() > QUEUED_DATA_MAX) entry->queuedData.pop_front(); + MarkBufferReused(*entry); + AudioOut2QueuedData queued{entry->data, CopyGrain(*entry)}; entry->repeatedData = entry->data != nullptr && (entry->lastBuffer == nullptr || entry->lastBuffer == entry->data) ? entry->data : nullptr; + entry->repeatedPcm = queued.pcm; + entry->repeatedReused = false; + entry->queuedData.push_back(std::move(queued)); + if (entry->queuedData.size() > QUEUED_DATA_MAX) entry->queuedData.pop_front(); if (entry->data != nullptr) entry->lastBuffer = entry->data; entry->dataSets++; verdict = "pcm data pointer"; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index b1d5725eca..27d938c27c 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -286,6 +286,28 @@ void TestRenderingSizedByLevelStaysWithinQueueDepth() { RequirePlayed(played, order); } +void TestBufferReusedAcrossPortsKeepsEachPortsGrain(bool advanced) { + const auto voice = Grain(0.01f); + const std::vector silence(voice.size(), 0.0f); + auto shared = voice; + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + AudioOut2PortParam portParams{}; + portParams.port_type = portTypeMain; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + AudioOut2PortHandle other = 0; + Require(sceAudioOut2PortCreate(context, &portParams, &other) == 0); + SetData(port, shared.data()); + shared = silence; + SetData(other, shared.data()); + if (advanced) Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + SetData(port, nullptr); + SetData(other, nullptr); + }, advanced); + RequirePlayed(played, {&voice}); +} + } int main() { @@ -299,5 +321,7 @@ int main() { TestAdvancingPortWithoutNewBufferPlaysSilence(); TestRewrittenBufferRepeats(); TestRenderingSizedByLevelStaysWithinQueueDepth(); + TestBufferReusedAcrossPortsKeepsEachPortsGrain(false); + TestBufferReusedAcrossPortsKeepsEachPortsGrain(true); return 0; } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 43372d1a15..7151588a5a 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -19,7 +19,7 @@ - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped -- [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented +- [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Each set also copies the grain from the buffer: once another port of the context is given the same buffer, the grains already queued with it (and its repeat) play that copy instead of the buffer's later contents, as PPSA08587 fills one buffer and hands it to each of its three ports in turn before advancing. A buffer no other port is given is still read when the grain is pushed. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented - [Legacy vibration ports](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp): PPSA99902's [public input wrapper](https://github.com/Nazky/UnleashedRecomp-Prospero/blob/2ce067a1d51bb58670c7ee4a3aed893c504da805/UnleashedRecomp/hid/driver/sdl_hid.cpp#L316) opens type 10 with 256 stereo S16 or F32 frames at 48000 Hz. Mono duplicates its channel as AudioOut2 does; wider vibration layouts throw. Native controller output uses AudioOut2's four-channel layout, or its six-channel PulseAudio mapping. Without a controller sound card the existing RMS motor transform is applied to segments of at most 256 frames, scheduled by the legacy queue's frame count and rate; drain and close remove only that port's segments. Concurrent sources combine by the maximum per motor, and the pad consumer uses this approximation only for controllers other than DualSense. This host timing and motor model is not a console measurement or waveform reproduction; legacy native output is unmeasured on hardware, and user routing retains the single-controller limitation. AudioOut2 retains its separate 80 ms peak hold. - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws